123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702 |
- import commonmark
- import json
- import os
- import dokk.sparql as sparql
- import urllib.parse
- from bottle import abort, get, post, redirect, request, response, route, static_file
- from datetime import datetime, timezone
- from dokk import article_template, graph, graph_public, settings, template, url, user
- from string import Template
- class requires_signin(object):
- """
- This is a decorator used for controller to make sure a user is signed in
- before entering the controller.
- """
-
- def __init__(self, controller):
- self.controller = controller
- def __call__(self, *args, **kargs):
- the_user = user.get_from_session()
-
- if not the_user:
- return redirect(url('signin') + '?next=' + urllib.parse.quote(request.url, safe=''))
- else:
- return self.controller(*args, **kargs)
- class requires_signout(object):
- """
- This is a decorator used for controller to make sure a user is signed out
- before entering the controller.
- """
-
- def __init__(self, controller):
- self.controller = controller
- def __call__(self, *args, **kargs):
- the_user = user.get_from_session()
-
- if not the_user:
- return self.controller(*args, **kargs)
- else:
- return redirect(url('user', username=the_user['username']))
- #### HACK: This is used to retrieve documentation, but it should not stay here!
- #### The only purpose of these routes is to maintain compatibility with
- #### the old website until I find a better solution.
- ###############################################################################
- @get('/documentation', name='documentation')
- def docs():
- """List of documentations and versions."""
-
- # Some page attributes
- page = { 'title': 'Documentation' }
-
- # Get list of available docs
- docs = sparql.query_public("""
- PREFIX : <dokk:/>
- PREFIX dokk: <https://ontology.dokk.org/>
- PREFIX schema: <http://schema.org/>
-
- DESCRIBE *
- WHERE
- {
- ?docs a dokk:Documentation .
- }
- """)
-
- docs = docs['@graph']
-
- return template('documentation/index.html', dokk=page, docs=docs)
- @get('/documentation/<id>', name='documentationproject')
- def documentationproject(id):
- """List of documentations and versions."""
-
- # Some page attributes
- page = { 'title': '' }
-
- doc = sparql.query_public(Template ("""
- PREFIX : <dokk:>
- PREFIX dokk: <https://ontology.dokk.org/>
- PREFIX schema: <http://schema.org/>
-
- DESCRIBE *
- WHERE
- {
- ?doc a dokk:Documentation ;
- dokk:id "$id" .
- }
- """).substitute({
- 'id': id
- }))
-
- page['title'] = doc['name'] + ' documentation'
- # doc = doc['@graph']
-
- return template('documentation/project.html', dokk=page, doc=doc)
- @get('/documentation/<docname>/<release>/', name='documentationdocs')
- @get('/documentation/<docname>/<release>/<path:path>/')
- def docs_index(docname, release, path=''):
- location = settings['dokk.archive'] + '/documentation/html/' + docname + '/' + release + '/'
-
- return static_file(path + '/index.html', root=location)
- @get('/documentation/static/<file:path>', name='documentationstatic')
- def docsstatic(file):
- return bottle.static_file(file, root=self.root+'static')
-
- @get('/documentation/<docname>/<release>/<filename:path>')
- def docs_static(docname, release, filename):
- location = settings['dokk.archive'] + '/documentation/html/' + docname + '/' + release + '/'
-
- return static_file(filename, root=location)
- ###############################################################################
- @route('/<filename:re:(favicon.ico|robots.txt)>')
- def public_assets(filename):
- """
- Tell Bottle where it can find some special files that are not inside /static
- This is needed because favicon.ico, robots.txt, and other files are expected
- to be found under the root path, and they should not be misunderstood as
- node IDs.
-
- TODO: find a better way to serve these static files.
-
- :param filename: the name of the file
- :return:
- """
-
- return static_file(filename, root='./')
- @get(settings['dokk.reserved_path'] + '/static/<filename:path>', name='static_assets')
- def static_assets(filename):
- """
- Path containing the static files.
-
- :param filename: The name of the static asset.
- :return:
- """
-
- return static_file(filename, root='./dokk/static')
- @get(settings['dokk.reserved_path'] + '/explore', name='explore')
- def explore():
- """
- Explore the DOKK graph.
- """
-
- return template('explore.html')
- @get(settings['dokk.reserved_path'] + '/explorenodes')
- def explore_nodes():
- """
- The "explore" controller loads the page template, from where a AJAX request
- is sent to this controller to retrieve the list of nodes to be displayed.
- """
-
- return graph.get_topics()
- @get(settings['dokk.reserved_path'] + '/search', name='search')
- def search():
- """
- Search the DOKK.
- """
-
- if 'q' not in request.GET:
- return redirect(url('index'))
-
- search_term = request.GET.getunicode('q').strip()
-
- if len(search_term) == 0:
- return redirect(url('index'))
-
- results = graph.search_articles(search_term, limit=100)['results']['bindings']
-
- return template('search.html', search_term=search_term, results=results)
- @get(settings['dokk.reserved_path'] + '/suggest', name='suggest')
- def suggest():
- """
- Search suggestions when typing in the DOKK search bar.
- """
-
- if 'q' not in request.GET:
- return ''
-
- search_term = request.GET.getunicode('q').strip()
-
- if len(search_term) == 0:
- return ''
-
- articles = graph_public.list_topics(search_term)['results']['bindings']
-
- return template('suggest.html', search_term=search_term, articles=articles)
- @get(settings['dokk.reserved_path'] + '/editor', name='editor')
- @requires_signin
- def dokk_editor():
- return template('editor/index.html')
- @post(settings['dokk.reserved_path'] + '/editor')
- @requires_signin
- def dokk_editor_new_node():
- """
- Handle the <form> POSTs from the Editor's homepage to create new
- topics/nodes/templates/queries. This controller basically just takes the POST
- data and redirects the user to the edit page.
- """
-
- if 'type' not in request.forms or \
- 'id' not in request.forms:
- return redirect(url('editor'))
-
- type = request.forms.getunicode('type')
- id = request.forms.getunicode('id')
-
- if type not in [ 'topic', 'template', 'query' ]:
- return redirect(url('editor'))
-
- return redirect(url('edit', type=type, id=id))
- @get(settings['dokk.reserved_path'] + '/editor/<type:page>/<id:path>', name='edit')
- @requires_signin
- def edit(type, id):
- """
- Edit a page.
-
- :param type: the type of information to edit (article, template, ...)
- :param id: ID of the node to edit.
- """
-
- # Normalize page ID
- normalized_id = graph.normalize_name(id)
- if normalized_id != id:
- return redirect(url('edit', type=type, id=normalized_id))
-
- # Read existing page
- page = type + '/' + id
- if graph.exists(page):
- revision = graph.get_last_revision(page)
- content = revision['content']
- revision_number = revision['dokk:revision_number']
- else:
- revision_number = None
-
- if type == 'topic':
- content = template('node/topic.ttl', node_id=id)
- elif type == 'article':
- content = template('node/article.md')
- elif type == 'query':
- content = template('node/query.sparql')
- elif type == 'template':
- content = template('node/template.html')
- else:
- content = ''
-
- return template('editor/edit.html',
- view='edit',
- type=type, node_id=id, content=content,
- revision_number=revision_number)
- @post(settings['dokk.reserved_path'] + '/editor/<type:page>/<id:path>')
- @requires_signin
- def save_edits(type, id):
- """
- Save user edits for a page.
- """
-
- # Retrieve user
- author = user.get_from_session()
-
- if not author:
- return redirect(url('index'))
-
- # Retrieve <form> data
- rev_number = request.forms.getunicode('revision').strip()
- content = request.forms.getunicode('content').strip()[:10485760]
- edit_summary = request.forms.getunicode('edit_summary').strip()[:1024]
-
- # Ignore completely if there is no edit_summary. This is just a bad request.
- if not edit_summary:
- return redirect(url('node', id=id))
-
- # The ID for this page
- page = type + '/' + id
-
- # Save a new revision for this page
- added = graph.add_revision(page, rev_number, author['username'], content, edit_summary)
-
- # If the revision hasn't been added, there was a conflict. Show merge page.
- if not added:
- current_revision = graph.get_last_revision(page)
-
- return template('editor/merge.html',
- view='merge',
- type=type, node_id=id, content=content,
- current_revision=current_revision,
- revision_number=current_revision['dokk:revision_number'],
- edit_summary=edit_summary)
-
- # Revision was added successfully!
-
- current_revision = graph.get_last_revision(page)
-
- if type == 'article':
- graph.add_article(id)
- return redirect(url('node', id=id))
-
- if type == 'file':
- graph.add_file(id)
- return redirect(url('revision', number=current_revision['dokk:revision_number'], type=type, id=id))
-
- if type == 'query':
- graph.add_query(id)
- return redirect(url('revision', number=current_revision['dokk:revision_number'], type=type, id=id))
-
- if type == 'template':
- graph.add_template(id)
-
- return redirect(url('revision', number=current_revision['dokk:revision_number'], type=type, id=id))
-
- if type == 'topic':
- graph.add_topic(id)
- return redirect(url('node', id=id))
- @get(settings['dokk.reserved_path'] + '/revisions/<type>/<id:path>', name='revisions')
- def show_revisions(type, id):
- """
- Show a list of revisions for one page.
-
- :param type: the type of information to edit (article, template, ...)
- :param id: ID of the node to edit.
- """
-
- page_name = type + '/' + id
- revisions = graph.list_revisions(page_name)['results']['bindings']
-
- return template('editor/revisions.html',
- view='revision',
- type=type, node_id=id, page_name=page_name,
- revisions=revisions)
- @get(settings['dokk.reserved_path'] + '/revision/<number:int>/<type>/<id:path>', name='revision')
- def show_revision(number, type, id):
- """
- Show an old revision.
-
- :param number: The revision number.
- :param type: the type of information to edit (article, template, ...)
- :param id: ID of the node to edit.
- """
-
- page_name = type + '/' + id
- revision = graph.get_revision(page_name, number)
- current_revision = graph.get_last_revision(page_name)
-
- return template('editor/old_revision.html',
- view='revision',
- type=type, node_id=id, page_name=page_name,
- revision=revision, current_revision=current_revision)
- @get(settings['dokk.reserved_path'] + '/upload', name='upload')
- @requires_signin
- def dokk_upload():
- return template('editor/upload.html')
- @post(settings['dokk.reserved_path'] + '/upload')
- @requires_signin
- def dokk_upload_handle():
- """
- Handle uploaded file.
- """
-
- # Store errors here (if file cannot be saved for some reasons)
- errors = []
-
- # The file being uploaded
- blob = request.files.get('blob')
-
- # Retrieve form data
- # Note about Bottle: request.forms['key'] or request.forms.get("key") return
- # the raw parameter, the byte string. Instead, request.forms.key and
- # request.forms.getunicode("key") return the UTF-8 string. Normally we use
- # request.forms.getunicode("key") for consistency and also to avoid any doubts,
- # but when using <form enctype="multipart/form-data"> the request body is
- # already a UTF-8 string and request.forms.getunicode() tries to encode a
- # string that is already UTF-8 encoded. So the value that is returned is either
- # an empty string or None, therefore we use the "raw" value from the request.
- # It's probably a Bottle bug that needs fixing upstream.
- file = {
- 'name': request.forms['name'],
- 'license': request.forms['license'],
- 'source': request.forms['source'],
- 'description': request.forms['description']
- }
-
- # Clear input data
- file['name'] = file['name'].strip()
- file['name'] = graph.normalize_name(file['name'])
- file['name'] = file['name'].replace('/', '-')
- file['license'] = file['license'].strip()
- file['source'] = file['source'].strip()
- file['description'] = file['description'].strip()
-
- # Where to save the file
- file['path'] = settings['dokk.archive'] + '/' + file['name']
-
- # File name must have an extension
- if '.' not in blob.filename:
- errors.append('no_extension')
-
- # Validate file name
- if len(file['name']) == 0:
- errors.append('name')
-
- # Validate file extension
- if os.path.splitext(blob.filename)[1] != os.path.splitext(file['name'])[1]:
- errors.append('extension_mismatch')
-
- # Validate license
- if len(file['license']) == 0:
- errors.append('license')
-
- # Make sure a file with this name does not exist
- if os.path.exists(file['path']):
- errors.append('path')
-
- if len(errors) > 0:
- return template('editor/upload.html', errors=errors, file=file)
-
- # Everything fine! We can save the file
-
- # Retrieve user
- author = user.get_from_session()
-
- if not author:
- return redirect(url('editor'))
-
- # Store blob to filesystem
- blob.save(file['path'])
-
- # Automatically create a new revision of this file's properties
- graph.add_revision(
- 'file/' + file['name'],
- None,
- author['username'],
- template('node/file.ttl', filename=file['name'],
- license=file['license'],
- primary_source=file['source'],
- description=file['description']),
- 'New file upload.')
-
- # Automatically create a new node with type "dokk:File"
- graph.add_file(file['name'])
-
- # Redirect to the file edit page
- return redirect(url('edit', type='file', id=file['name']))
- @get(settings['dokk.reserved_path'] + '/templates', name='templates')
- @requires_signin
- def templates():
- """
- Show a list of all templates available on this DOKK instance.
- """
-
- templates = graph.list_templates()['results']['bindings']
-
- return template('editor/templates.html', templates=templates)
- @get(settings['dokk.reserved_path'] + '/queries', name='queries')
- @requires_signin
- def queries():
- """
- Show a list of all queries available on this DOKK instance.
- """
-
- queries = graph.list_queries()['results']['bindings']
-
- return template('editor/queries.html', queries=queries)
- @get(settings['dokk.reserved_path'] + '/files', name='files')
- @requires_signin
- def files():
- """
- Show a list of all uploaded files on this DOKK instance.
- """
-
- all_files = [ f for f in os.listdir(settings['dokk.archive'])
- if os.path.isfile(os.path.join(settings['dokk.archive'], f))]
-
- return template('editor/files.html', files=all_files)
- @get(settings['dokk.reserved_path'] + '/file/<filename>', name='file')
- def show_file(filename):
- """
- Show a file and its properties. To return the raw binary, see "show_blob()"
- """
-
- """
- path = settings['dokk.archive'] + '/' + filename
-
- if not os.path.isfile(path):
- return abort(404)
- """
-
- node = graph.get_file(filename)
-
- return template('editor/file.html', filename=filename, node=node)
- @get(settings['dokk.reserved_path'] + '/blob/<filename>', name='blob')
- def show_blob(filename):
- """
- Return a raw file from the repository.
- """
-
- return static_file(filename, root=settings['dokk.archive'])
- @get(settings['dokk.reserved_path'] + '/signin', name='signin')
- @requires_signout
- def signin():
- """
- The sign in page.
- """
- return template('signin.html')
- @post(settings['dokk.reserved_path'] + '/signin')
- @requires_signout
- def signin_check():
- """
- Check sign in form.
- """
-
- username = request.forms.getunicode('username')
- password = request.forms.getunicode('password')
- remember = 'remember' in request.forms
-
- if not username or not password:
- return template('login.html',
- flash = 'Bad login!')
-
- # Retrieve user from database
- the_user = user.get_from_password(username, password)
-
- # Username/Password not working
- if 'username' not in the_user:
- return template('signin.html',
- flash = 'Bad login!')
-
- # Stop banned users
- if user.is_banned(username):
- return template('signin.html',
- flash = 'Bad login!')
-
- # ... Everything OK?
-
- # Start new browser session
- user.start_session(username, remember)
-
- # Redirect to URL if "?next=" is set in the URL
- if 'next' in request.GET:
- return redirect(request.GET.getunicode('next'))
-
- # otherwise redirect to homepage
- return redirect(url('editor'))
- @get(settings['dokk.reserved_path'] + '/register', name='register')
- @requires_signout
- def register():
- """
- Register new account.
- """
-
- return template('register.html')
- @post(settings['dokk.reserved_path'] + '/register')
- @requires_signout
- def register_new_account():
- """
- Check form for creating new account.
- """
-
- username = request.forms.getunicode('username')
- password = request.forms.getunicode('password')
-
- # Normalize username
- username = username.strip()
-
- if len(username) == 0:
- return None
-
- # Check if username already exists.
- if user.exists(username):
- return template('register.html',
- flash='Name already taken, please choose another one.')
-
- # Password too short?
- if len(password) < 8:
- return template ('register.html',
- flash = 'Password too short.')
-
- # Username OK, Password OK: create new user
- user.create(username, password)
-
- # Retrieve user (to check if it was created)
- new_user = user.get_from_password(username, password)
-
- # Something bad happened...
- if 'username' not in new_user:
- return template('register.html',
- flash = 'An error has occurred, please try again.')
-
- # Start session...
- user.start_session(username)
-
- # ... and go to the homepage
- return redirect(url('index'))
- @get(settings['dokk.reserved_path'] + '/signout', name='signout')
- @requires_signin
- def signout():
- """
- Logout user and return to homepage.
- """
-
- user.end_session()
-
- redirect(url('index'))
- @get(settings['dokk.reserved_path'] + '/user/<username>', name='user')
- def show_user(username):
- """
- Show a user homepage.
- """
-
- return template('user/homepage.html')
- @route(settings['dokk.reserved_path'])
- @route(settings['dokk.reserved_path'] + '/<path:path>')
- def reserved_paths(path=None):
- """
- Forbid users from editing any page starting with the reserved path.
- """
-
- return abort(403) # 403 - Forbidden
- @get('/<id:path>', name='node')
- def graph_node(id):
- """
- Read a node's article. This controller must be kept at the end,
- because it catches all routes that have not been caught by other routes.
- """
-
- # Normalize page ID
- normalized_id = graph.normalize_name(id)
-
- # Redirect to the correct page
- if normalized_id != id:
- return redirect(url('node', id=normalized_id))
-
- # This topic (node) doesn't exist in the database
- if not graph_public.topic_exists(normalized_id):
- response.status = 404 # Not found
- return template('article_not_found.html', node_id=id)
-
- # Retrieve topic node
- node = graph_public.get_topic(id)
-
- # Sort templates by name
- node_types = sparql.expand(node)[0]['@type']
- node_types.sort()
-
- # Move dokk:Topic in first position
- if 'https://ontology.dokk.org/Topic' in node_types:
- node_types.remove('https://ontology.dokk.org/Topic')
- node_types.insert(0, 'https://ontology.dokk.org/Topic')
-
- # Concatenate all templates into a single page
- page = ''
- for type_id in node_types:
- page += "{% include '" + type_id + "' %}\n"
-
- # Render templates for node types
- page = article_template(page + '\n', node=node)
-
- # Page variables
- dokk = { 'title': node['title'] if 'title' in node else node['id'].replace('_', ' ') }
-
- return template('article.html', dokk=dokk, node=node, page=page)
-
- @get('/', name='index')
- def index():
- """
- Homepage.
- """
-
- return template('homepage.html')
|