Commits

Anonymous committed 9d4fb2e

add password management.

Comments (0)

Files changed (18)

friendpaste/_design/auth/pasteid/map.js

+function(doc) {
+    if (doc.itemType == 'privacy')
+        emit(doc.pasteid, doc);
+}

friendpaste/application.py

 
 from friendpaste import settings
 from friendpaste import context_processors
-from friendpaste.urls import urls_map, all_views
+from friendpaste.urls import urls_map, all_views, OTHER_PATHS
 from friendpaste.utils import local, local_manager
 from friendpaste.http import FPRequest, session_store
-
+from friendpaste.models import Privacy
 
 class FriendpasteApp(object):
     def __init__(self):
     def dispatch(self, environ, start_response): 
         local.request = request = FPRequest(self, environ)
         local.url_adapter = adapter  = urls_map.bind_to_environ(environ)
-        
+
+        # get paste privacy here
+        cur_path = [p for p in request.path.split('/') if p]
+        if cur_path and cur_path not in OTHER_PATHS:
+            request.pasteid = cur_path[0]
+            request.privacy = Privacy.for_paste(local.db, request.pasteid)
+        else:
+            request.pasteid = None
+            request.privacy = None
+
         try:
             endpoint, args = adapter.match(request.path)
             response = self.views[endpoint](request, **args)
             
             
         if request.session.should_save:
+            print "save"
             max_age = settings.SESSION_COOKIE_AGE
             expires = time.time() + settings.SESSION_COOKIE_AGE
             session_store.save(request.session)

friendpaste/context_processors.py

 from friendpaste.template import register_contextprocessor
 from friendpaste.utils import local
 
+
 @register_contextprocessor
 def settings(request):
     return {
         'theme': request.session.get('theme', 'default'),
         'showlinenos': request.session.get('showlinenos', True)
-    }
+    }
+
+@register_contextprocessor
+def privacy(request):
+    can_edit = True
+    if hasattr(request, 'pasteid'):
+        authenticated = request.session.get(
+                '%s_authenticated' % request.pasteid, False)
+    else:
+        authenticated = False
+    privacy = 'open'
+    if hasattr(request, 'privacy'):
+        if request.privacy is not None:
+            privacy = request.privacy.privacy
+            if privacy in ['private', 'public'] and not authenticated:
+                can_edit = False
+    return {
+        'privacy': privacy,
+        'authenticated': authenticated,
+        'can_edit': can_edit
+    }
+    

friendpaste/models.py

 
     itemType = TextField(default='fork')
 
+class Privacy(Document):
+    pasteid = TextField()
+    privacy = TextField(default='open')
+    password = TextField(default='')
+    last_changed = DateTimeField()
+
+    itemType = TextField(default='privacy')
+
+    def store(self, db):
+        self.last_changed = datetime.utcnow()
+        super(Privacy, self).store(db)
+
+    @classmethod
+    def for_paste(cls, db, pasteid):
+        rows = cls.view(db, '_view/auth/pasteid', key=pasteid)
+        results = list(rows)
+        if results:
+            return results[0]
+        return None 
+
 class Paste(Document):
     pasteid = TextField(default='')
     title = TextField()
             elif old_data['edit_code'] != self.edit_code or \
                     old_data['locked'] != self.locked:
                 db[self.id] = self._data
+
         return self
        
     @classmethod

friendpaste/template.py

 from jinja2 import Environment
 from jinja2.loaders import FileSystemLoader
 from pygments import highlight, lexers, formatters
-from pygments.styles import get_all_styles
+
 
 from friendpaste import settings
 from friendpaste.http import FPResponse, FPRequest
 from friendpaste.utils import local, timesince, datetimestr_topython,\
 datetime_tojson
 
-template_env = Environment(loader=FileSystemLoader(settings.TEMPLATES_PATH))
+template_env = Environment(
+        loader=FileSystemLoader(settings.TEMPLATES_PATH))
 template_env.charset = 'utf-8'
 
+ALL_COLORSHEME = ['manni', 'perldoc', 'borland', 'colorful', 
+        'default', 'murphy', 'trac', 'fruity', 'autumn', 'bw', 
+        'emacs', 'pastie', 'friendly']
 DIFF_CHANGES = {
     'mod': 'Changed',
     'add': 'Added',
 template_env.globals['SITE_URI'] = settings.SITE_URI
 template_env.globals['ORBITED_PORT'] = settings.ORBITED_PORT
 template_env.globals['ORBITED_HOST'] = settings.ORBITED_HOST
-template_env.globals['ALL_COLORSHEME'] = list(get_all_styles())
+template_env.globals['ALL_COLORSHEME'] = ALL_COLORSHEME
 template_env.filters['rfc3339'] = datetime_tojson
 
 def re_escape(value):

friendpaste/urls.py

         'paste/embed': views.embed,
         'paste/reviews': views.snippet_reviews,
         'paste/review': views.snippet_review,
-        'paste/reviewset': views.snippet_reviews_set
+        'paste/reviewset': views.snippet_reviews_set,
+        'login': views.login,
+        'logout': views.logout
+
 }
 
 
     Rule('/_all_languages', endpoint='paste/all_languages'),
     Rule('/<id>_<rev>/raw', endpoint='paste/raw'),
     Rule('/<id>_<rev>/original', endpoint='paste/original'),
+    Rule('/<id>/logout', endpoint='logout'),
+    Rule('/<id>/login', endpoint='login'),
     Rule('/<id>/rss', endpoint='paste/rss'),
     Rule('/<id>/edit', endpoint='paste/edit'),
     Rule('/<id>/fork', endpoint='paste/fork'),
     Rule('/<id>.js', endpoint='paste/embed'),
     Rule('/<id>', endpoint='paste/view') 
 ])
+
+
+OTHER_PATHS = ['highlight', 'about', 'services', 'settings', '_all_languages']

friendpaste/views.py

 from pygments.formatters import HtmlFormatter
 from werkzeug import redirect
 from werkzeug.utils import url_quote
-from werkzeug.routing import NotFound
+from werkzeug.exceptions import Unauthorized, NotFound
 
 import simplejson as json
 
 from friendpaste import settings
 from friendpaste.feeds import RssFeed, RssFeedEntry 
 from friendpaste.http import FPResponse, send_json
-from friendpaste.models import Paste, Fork, Review
+from friendpaste.models import Paste, Fork, Review, Privacy
 from friendpaste.template import render_response, highlighter, pretty_type, shighlight, \
-render_template
+render_template, url_for, ALL_COLORSHEME 
 from friendpaste.utils import local, local_manager, datetimestr_topython,\
 strptime, make_hash, datetime_tojson, send_stomp_msg
 from friendpaste.utils.diff import get_unchanged_lines
 LEXERS_CHOICE = [('text', 'Plain text')] + _get_lexers()
 ALL_LEXERS=get_all_lexers()
 
-ALL_COLORSHEME = list(get_all_styles())
+
 
 PRIVACY_CHOICES = [
         ('open', 'Everyone can view and edit'),
         if field.data != "open" and not form.data['password']:
             raise ValidationError(u'Password is empty.')
 
+def login_required(f, privacy=['private']):
+    def _decorated(request, **kwargs):
+        authenticated = request.session.get(
+                '%s_authenticated' % request.pasteid, False)
+        mimetypes = request.accept_mimetypes
+        if request.privacy is not None:
+            if request.privacy.privacy in privacy and not authenticated:
+                if 'application/json' in mimetypes or request.is_xhr:
+                    raise Unauthorized
+                return redirect(url_for('login', 
+                    id=request.pasteid, next=request.path))
+        return f(request, **kwargs)
+    return _decorated
+           
+def not_logged(f):
+    def decorated(request, **kwargs):
+        authenticated = request.session.get('%s_authenticated' % request.pasteid)
+        if authenticated is not None:
+            redirect_url = request.values.get('next', '/')
+            return redirect(redirect_url)
+        return f(request, **kwargs)
+    return decorated
+
 def create_snippet(request):
     mimetypes = request.accept_mimetypes
     if request.method=='POST' and 'application/json' in mimetypes:
             code = d.get('edit_code', '')
             if code:
                 code = make_hash(code)
+
             s = Paste(
                     title=d.get('title', ""),
                     content=d.get('snippet'),
                     language=language,
-                    edit_code=code
+                    edit_code=code 
             )
             s.store(local.db)
+
+            p = Privacy(
+                pasteid = s.pasteid,
+                privacy = form.data['privacy'],
+                password = pwd
+            )
+            p.store(local.db)
         except:
             return send_json({'ok': False, 'reason': 'something wrong happend while saving data'})
 
         if code:
             code = make_hash(code)
 
+        pwd = form.data.get('password', '')
+        if pwd:
+            pwd = make_hash(pwd)
+
         s = Paste(
                 title=form.data['title'],
                 content=form.data['snippet'],
                 edit_code= code        
         )
         s.store(local.db)
+
+        p = Privacy(
+            pasteid = s.pasteid,
+            privacy = form.data['privacy'],
+            password = pwd
+        )
+        p.store(local.db)
+        request.session['%s_authenticated' % s.pasteid] = True
+        
         return redirect ('/%s' % s.pasteid)
 
     return render_response('paste/index.html', form=form)
 
+
 def edit_snippet(request, id):
     s = Paste.get_paste(local.db, id)
     if not s:
         
          # add latest reviews to unchanged lines
         unchanged = get_unchanged_lines(old_content,form.data['snippet'] )
-        print unchanged
 
         s.title=form.data['title']
         s.content = form.data['snippet']
         s.language = form.data['language']
+
         s.store(local.db)
         if s.nb_revision == old_revision:
             return redirect ('/%s?msg=%s' % (s.pasteid, url_quote("No changes detected.")))
 
         return redirect ('/%s' % s.pasteid)
     return render_response('paste/edit.html', form=form, snippet=s)
+edit_snippet = login_required(edit_snippet, privacy=['private', 'public'])
 
+@login_required
 def fork_snippet(request, id):
     s = Paste.get_paste(local.db, id)
     if not s:
         if code:
             code = make_hash(code)
 
+        pwd = form.data.get('password', '')
+        if pwd:
+            pwd = make_hash(password)
+
         s.forked = True
         s.store(local.db)
 
                 content=form.data['snippet'],
                 language=form.data['language'],
                 edit_code= code,
+                privacy=form.data['privacy'],
+                password=pwd,
                 fork = True,
                 fork_parent = s.pasteid,
                 forked_atrevision = s.nb_revision
 
     return render_response('paste/fork.html', form=form, snippet=s)
 
+@login_required
 def view_snippet(request, id):
     mimetypes = request.accept_mimetypes
     s = Paste.get_paste(local.db, id)
         error = True
     return render_response('paste/lock.html', snippet=s, error=error)
 
-    
+@login_required  
 def view_source(request, id):
     s = Paste.get_paste(local.db, id)
     if s is None:
         
     return render_response('paste/view_source.html', snippet=s)
 
+
 def view_rss(request, id):
     snippet, revisions = Paste.with_revisions(local.db, id, request.values.get('rev'))
     if snippet is None:
         raise NotFound
         
+    p = Privacy.for_paste(local.db, id)
+    if p.privacy == "private":
+        if p.password != make_hash(request.values.get('password', '')):
+            raise Unauthorized
+
+
     feed = RssFeed(
         title="Revisions to %s on Friendpaste" % (snippet.title and snippet.title or "snippet #%s" % snippet.pasteid),
         description = '',
      
     return feed.get_response()
 
+@login_required
 def view_revisions(request, id):
     snippet, revisions = Paste.with_revisions(local.db, id, request.values.get('rev'))
     
         
     return send_json({ 'highlighted': highlighted })
 
+@login_required
 def view_rawsnippet(request, id, rev):
     paste =  Paste.get_paste(local.db, id)
     if paste is None:
     response.headers['content-type'] = 'text/plain'
     return response
 
+@login_required
 def view_original(request, id, rev):
     snippet = Paste.get_paste(local.db, id)
     if not snippet:
         response.headers['content-disposition'] =  "filename=%s.txt" % snippet.pasteid
     return response
 
-
+@login_required
 def view_changeset(request, id):
     snippet = Paste.get_paste(local.db, id)
     revid = request.values.get('rev', None)
     return render_response('paste/diff.html', unidiff=unidiff, difft=tabular,
             snippet=snippet, rev=snippet.nb_revision, old_rev=previous.nb_revision)
 
+
+@login_required
 def snippet_reviews(request, id, nb_line):
     rev = request.values.get('rev')
     if rev is not None:
     
     return send_json({ "ok": True, "r": review })
 
+@login_required
 def snippet_reviews_set(request, id, nb_line):
     rev = request.values.get('rev')
     if rev is not None:
         "count": len(review['reviews'])
     })
 
+@login_required
 def snippet_review(request, id, nb_line):
     rev = request.values.get('rev')
 
     languages.sort()
     return send_json(dict(languages))
 
+
 def embed(request, id):
     p = Paste.get_paste(local.db, id)
     if p is None:
         raise NotFound
 
+    p = Privacy.for_paste(local.db, id)
+    if p.privacy == "private":
+        if p.password != make_hash(request.values.get('password', '')):
+            raise Unauthorized
+
+
     theme = request.session.get('settings_theme', 'default')
     response = render_response('paste/embed.js', snippet=p, theme=theme)
     response.headers['content-type'] = 'application/javascript'
 
     return send_json( { 'ok': True, 'settings': settings } )
 
+@not_logged
+def login(request, id):
+    mimetypes = request.accept_mimetypes
+    next = request.values.get('next', '/%s' % id)
+    error = ""
+
+    if request.method == "POST":
+        if 'application/json' in mimetypes:
+            d = json.loads(request.data)
+            password = d.get('password', '')
+            if not password:
+                return send_json({ 'ok': False, 'reason': 'Password is empty'})
+            if password == request.privacy.password:
+                request.session['%s_authenticated' % id] = True
+                return send_json({'ok': True})
+            raise Unauthorized
+
+        password = request.form.get('password', '')
+        next = request.form.get('next', '/%s' % id)
+        if password:
+            if make_hash(password) == request.privacy.password:
+                request.session['%s_authenticated' % id] = True
+                return redirect(next)
+            error = u"Password is invalid."
+        else:
+            error = u"Password is empty."
+            
+    return render_response("paste/login.html", pasteid=id, 
+            next=next, error=error)
+
+def logout(request, id):
+    key = '%s_authenticated' % id
+    if request.privacy.privacy in ['public', 'open']:
+        next = "/%s" % id
+    else:
+        next = request.values.get('next', '/')
+    if key in request.session:
+        del request.session[key]
+    return redirect(next)
+
+
 
 # generic views
 def about(request):

static/css/src/colors.css

 h1, h2, h3, h4, h5, h6 {color:#111;}
 
 
-a:link {
-    color: #2e8696;
-}
+a:link, 
 a:visited {
     color: #2e8696;
 }
 
+
+
 #main_nav li {
     background-color: #899834;
 }
 #site_header a:link, #site_header a:visited {
     color: #fff;
 }
-
+.authBox a:link,
+.authBox a:visited {
+    color: #2e8696;
+}
 form label {
     color: #333;
 }
     border-color: #f7f7f0;
 }
 
+div.errors,
 li.errors {
     background-color: #ff9898;
     padding: 0.3em;

static/css/src/layout.css

     margin-left: 80px;
     position: relative;
     top: 35px;
+    z-index: 100;
 }
 #main_nav li {
     display: block;
     float: left
 }
 
+#site_header .authBox {
+    display: block;
+    width: 400px;
+    top: 5px;
+    margin-left: 80px; 
+    float: left;
+    position: relative;
+    z-index: 90;
+}
+
+.authBox a {
+    padding: 0.5em 1em 0.5em 1em;
+}
 /* content */
 .content-wrapper {
     background: transparent url(../../images/home-back.png) no-repeat 100px 0pt;
 }
 
 /* paste form */
-#snippet_edit,
-#snippet_view,
+#edit,
+#view,
 #revisions,
 #new_snippet,
 #s_snippet_actions {
 #bottoma li {
     display: block;
     float: left;
-    width: 100px;
+    /*min-width: 100px;*/
     margin-right: 5px;
     padding: 0;
 }
 }
 
 
+
+div.errors,
 li.errors {
     padding: 0.3em;
     border: 1px solid;

static/css/src/typography.css

     text-decoration: underline;
 }
 
+.authBox a {
+    font-weight: bold;
+}
+
+
 #nav li {
     font-size: 1.2em;
     font-weight: bold;
Add a comment to this file

static/images/closed.png

Added
New image
Added
New image

static/js/src/friendpaste.js

 
     constructor: function() {
         var self = this;
+
+        
+
         /* set handlers */
         var edit = document.querySelector(".e");
         var cancel = document.querySelectorAll(".cancel");
         if (history) 
             history.addEventListener("click",this.do_history.bindAsEventListener(this), false);
 
-        this.snippet = document.querySelector('#snippet_view');
-        this.snippet_edit = document.querySelector('#snippet_edit');
+        this.snippet = document.querySelector('#view');
+        this.snippet_edit = document.querySelector('#edit');
         this.revisions = document.querySelector('#revisions');
         this.bdelete = document.querySelector("#snippet_actions li.delete");
 
             this.listenPasteActions();
         }
 
+        var targets = /#(.+)$/.exec(window.location);
+        
+        if (targets[1] == "edit") {
+            this.snippet.classList.add("hidden");
+            this.snippet_edit.classList.remove("hidden");
+        } else {
+            this.editing = true;
+        }
         this.editing = false;
         this.show_history = false;
         

templates/base.html

 <body class="{{ theme }}">
     <header id="site_header">
         <h1><a href="/">Friendpaste</a></h1>
+        {% block auth %}{% endblock %}
         <ul id="main_nav">
             <li><a href="/">New paste</a></li>
             <li><a href="/services">Services</a></li>

templates/paste/diff.html

 {% endblock %}
 
 {% block content %}
-<section id="snippet_view">
+<section id="view">
     <article id="snippet">
         <header>
             <h2><a href="/{{ snippet.pasteid }}">{{ snippet.pasteid

templates/paste/edit.html

 %}#{{ snippet.pasteid }}{% endif %}{%endblock %}
 {% block content %}
 
-<section id="snippet_edit">
+<section id="edit">
 
     <form id="snippet-edit" name="fpaste" action="/{{ snippet.pasteid }}/edit" method="post">
         {% if form.errors %}

templates/paste/login.html

+{% extends "base.html" %}
+
+{% block title %}
+    Login to this paste
+{% endblock %}
+
+{% block header %}{% endblock %}
+
+{% block content %}
+<section id="new_snippet">
+    <form name="fpassword" id="fpassword" action="{{ url_for('login',id=pasteid) }}" method="post">
+        <input type="hidden" name="next" value="{{ next }}" />
+        <h2>Please enter a pasword for the paste #{{ pasteid }}</h2>
+        {% if privacy == 'private' %}
+        <h4>Password is required to view and edit paste.</h4>
+        {% endif %}
+        {% if privacy == 'public' %}
+        <h4>Password is required to edit paste.</h4>
+        {% endif %}
+        
+        
+        {% if error %}
+        <p class="errors">{{ error }}</p>
+        {% endif %} 
+        <ol>
+            <li><input type="password" name="password" id="password" value="" /></li>
+            <li><input type="submit" id="spassword" name="spassword" value="Login"></li>
+        </ol>
+    </form>
+</section>                        
+{% endblock %}
+
+

templates/paste/view.html

 
 {% block title %}{% if snippet.title %}{{ snippet.title }}{% else %}Paste #{{ snippet.pasteid }}{% endif %} {% endblock %}
 
+
+{% block auth %}
+    {% if privacy != 'open' and not can_edit %}
+    <div class="authBox"><a href="{{ url_for('login', id=snippet.pasteid,rev=snippet.nb_revision) }}&next={{ url_for('paste/view', id=snippet.pasteid, rev=snippet.nb_revision) }}">Login to edit</a></div>
+    {% endif %}
+
+    {% if privacy!=open and can_edit %}
+    <div class="authBox"><a href="{{ url_for('logout', id=snippet.pasteid) }}?next={{ url_for('paste/view', id=snippet.pasteid, rev=snippet.nb_revision) }}">Logout from this paste</a></div>
+    {% endif %}
+
+{% endblock %}
+
 {% block head %}
 <link rel="alternate" href="/{{ snippet.pasteid }}/rss" type="application/rss+xml" class="rss" title="RSS Feed" />
 
 {% endblock %}
 
 {% block content %}
-<section id="snippet_view">
+<section id="view">
     <article id="snippet">
         <header>
             <div id="snippet_title"> 
         <footer>
             <div id="bottoma">
                 <ul>
-                    {% if not snippet.locked %}
+                    
                     <li>
-                        
+                        {% if not snippet.locked and can_edit %}
                         <form action="/{{ snippet.pasteid }}/edit" method="get">
                             <input type="submit" class="e" value="Edit paste" />
                         </form>
+                        {% else %}
+                        <form name="flogin" action="/{{ snippet.pasteid }}/login" method="get">
+                            <input type="hidden" name="next" value="/{{ snippet.pasteid }}#edit" />
+                            <input type="submit" value="Login to edit" />
+                        </form>
+                        {% endif %}
+
                     </li>
-                    {% endif %}
                     <li>
                         <form action="/{{ snippet.pasteid }}/fork"
                             method="get">
 </section>
 
 {% if not snippet.locked %}
-<section id="snippet_edit" class="hidden">
+<section id="edit" class="hidden">
     <form id="fedit" name="fedit" method="post" action="/{{
         snippet.pasteid }}/edit" class="aligned">
         <ol>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.