├── .gitignore ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.creole ├── scripts └── manage.sh ├── setup.py ├── testproject ├── __init__.py ├── logger.conf ├── manage.py ├── settings.py ├── templates │ └── testproject │ │ └── url_info.html ├── urls.py ├── views.py ├── weave_server.py └── weave_server_tester.py ├── update.sh └── weave ├── __init__.py ├── admin.py ├── app_settings.py ├── constants.py ├── decorators.py ├── migrations ├── 0001_initial.py ├── 0002_add_field_wbo_ttl.py ├── 0003_add_field_payload_size.py └── __init__.py ├── models.py ├── templates ├── 404.html ├── 500.html └── weave │ └── info_page.html ├── tests.py ├── urls.py ├── utils.py └── views ├── __init__.py ├── misc.py ├── sync.py └── user.py /.gitignore: -------------------------------------------------------------------------------- 1 | local_settings.py 2 | *.pyc 3 | *.log 4 | *~ 5 | /django_sync_server.egg-info 6 | /dist 7 | *.db3 8 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | PRIMARY AUTHORS are and/or have been (alphabetic order): 2 | 3 | * Diemer, Jens 4 | Main Developer since the first code line. 5 | ohloh.net profile: 6 | Homepage: 7 | 8 | * Fladischer, Michael 9 | Email: 10 | 11 | CONTRIBUTORS are and/or have been (alphabetic order): 12 | * Schier, Alexander 13 | Email: 14 | github: 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All rights reserved. 2 | 3 | 4 | django-weave is free software; you can redistribute it and/or modify it 5 | under the terms of the GNU General Public License version 3 or later as published 6 | by the Free Software Foundation. 7 | 8 | complete GNU General Public License version 3: 9 | http://www.gnu.org/licenses/gpl-3.0.txt 10 | 11 | German translation: 12 | http://www.gnu.de/documents/gpl.de.html 13 | 14 | 15 | copyleft 2010 by the django-weave team, see AUTHORS for more details. 16 | 17 | 18 | SVN info: 19 | $LastChangedDate: 2008-06-05 16:33:09 +0200 (Do, 05 Jun 2008) $ 20 | $Rev: 1635 $ 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS README.creole MANIFEST.in 2 | recursive-include testproject * 3 | recursive-exclude * *.py[co] 4 | -------------------------------------------------------------------------------- /README.creole: -------------------------------------------------------------------------------- 1 | = description 2 | 3 | django-sync-server (formerly 'django-weave') is a reusable application which implements a Firefox sync server for Django. 4 | 5 | The Server works up to FxSync v1.16.x (Firefox v14) Well tested also with SeaMonkey v2.3 6 | 7 | How to create a django-sync-server test virtualenv: [[http://code.google.com/p/django-sync-server/wiki/CreateTestEnvironment|CreateTestEnvironment Wiki page]] 8 | 9 | 10 | == What is Firefox Sync? 11 | 12 | Firefox Sync (formerly Mozilla Labs Weave Browser Sync) is a free browser add-on from Mozilla Labs that keeps your bookmarks, saved passwords, browsing history and open tabs backed up and synchronized, with end-to-end encryption for your privacy and security. 13 | 14 | 15 | == sourcecode 16 | 17 | Our code hosted on [[http://github.com/jedie/django-sync-server|github.com/jedie/django-sync-server]] 18 | 19 | Clone our git repo: 20 | 21 | {{{ 22 | git clone git://github.com/jedie/django-sync-server.git 23 | }}} 24 | 25 | A git clone also exist on [[https://code.google.com/p/django-sync-server/source/list|google code]]. 26 | 27 | == download 28 | 29 | Python packages available on: [[http://pypi.python.org/pypi/django-sync-server/]] 30 | 31 | Unofficial debian packages: [[http://debian.fladi.at/pool/main/d/django-sync-server/]] 32 | 33 | == knwon bugs 34 | 35 | With django 1.4 you will be run into a known bug, fi you create a new user. Please read the followed issues, with a work-a-round: 36 | * https://github.com/jedie/django-sync-server/issues/8 37 | 38 | = migrate 39 | 40 | * v0.3.0 to v0.4.0 41 | 42 | We used django-south to change the existing models. Do this: 43 | 44 | {{{ 45 | ~$ cd django_sync_server_env 46 | ~/django_sync_server_env$ source bin/activate 47 | (django_sync_server_env)~/django_sync_server_env$ pip install South 48 | (django_sync_server_env)~/django_sync_server_env$ cd src/django-sync-server/testproject 49 | (django_sync_server_env)~/django_sync_server_env/src/django-sync-server/testproject$ ./manage.py syncdb 50 | (django_sync_server_env)~/django_sync_server_env/src/django-sync-server/testproject$ ./manage.py migrate weave 0001 --fake 51 | (django_sync_server_env)~/django_sync_server_env/src/django-sync-server/testproject$ ./manage.py migrate weave 52 | }}} 53 | Note: After South install, you must insert "south" in INSTALLED_APPS list in our own settings.py see also: [[https://github.com/jedie/django-sync-server/commit/452668fb671662a15da2faf1e1c1f642d744b5dc#diff-1]] 54 | 55 | 56 | = history 57 | 58 | * v0.4.2 - 27.07.2012 59 | ** Bugfix in info_page() page: Use RequestContext(), so that inherit template can use variables from context processors 60 | ** remove git timestamp from version string 61 | * v0.4.1 62 | ** Bugfix to support sync with Firefox v3.6 - v5 (see: [[https://github.com/jedie/django-sync-server/issues/11]] ) 63 | * v0.4.0 64 | ** Updates to FxSync API 1.1 (see: [[https://github.com/jedie/django-sync-server/issues/11]] ) 65 | ** Create a info page on root url 66 | * v0.3.0 67 | ** Add work-a-round for username longer than 30 characters (see: [[https://github.com/jedie/django-sync-server/issues/8]] ) 68 | ** Add DONT_USE_CAPTCHA and DEBUG_REQUEST to app settings. 69 | * v0.2.1 70 | ** Some updates for django v1.2 API changes 71 | ** Change version string and add last commit date 72 | * v0.2.0 73 | ** django-sync-server own basic auth function can be disabled via app settings. 74 | * v0.1.7 75 | ** 'django-weave' was renamed to 'django-sync-server' 76 | * v0.1.6 77 | ** Bugfix checking weave api version from url. 78 | ** Add a tiny info root page to testproject. 79 | * v0.1.5 80 | ** Changes to establish compatibility with Weave client v1.2b3 81 | * v0.1.4 82 | ** split weave app and testproject 83 | * v0.1.3 84 | ** Remove dependency on django-reversion 85 | ** change Collection sites ManyToManyField to a normal ForeignKey 86 | * v0.1.2 87 | ** many code cleanup and bugfixes 88 | ** remove django-tools and django-reversion decencies 89 | * v0.1.0pre-alpha 90 | ** sync works 91 | * v0.0.1 92 | ** initial checkin 93 | 94 | = donation 95 | 96 | * [[http://flattr.com/thing/181551/django-sync-server|Flattr this!]] 97 | * Send [[http://www.bitcoin.org/|Bitcoins]] to [[https://blockexplorer.com/address/1JkiTSEwSybs8drWUNnpL4ndFQvKpGZNPX|1JkiTSEwSybs8drWUNnpL4ndFQvKpGZNPX]] 98 | 99 | = links 100 | 101 | | Project page | https://code.google.com/p/django-sync-server/ 102 | | GitHub | https://github.com/jedie/django-sync-server 103 | | PyPi | http://pypi.python.org/pypi/django-sync-server 104 | | Firefox Sync homepage | https://wiki.mozilla.org/Firefox_Sync 105 | | IRC | [[http://www.pylucid.org/permalink/304/irc-channel|#pylucid on freenode.net]] 106 | 107 | more links about firefox SyncFX: [[http://code.google.com/p/django-sync-server/wiki/SyncLinks|SyncLinks Wiki page]] 108 | 109 | Needfull informations are in [[http://code.google.com/p/django-sync-server/w/list|out google code Wiki]]: 110 | 111 | * [[http://code.google.com/p/django-sync-server/wiki/DebugHelp|How to debug]] 112 | * [[http://code.google.com/p/django-sync-server/wiki/WeaveSettings|How to change django/weave settings with a local_settings.py]] 113 | * [[http://code.google.com/p/django-sync-server/wiki/CreateTestEnvironment|Create a django-sync-server test virtualenv]] 114 | * [[http://code.google.com/p/django-sync-server/wiki/HTTPSDevelopment| How to use HTTPS with Django `runserver` command]] 115 | 116 | 117 | -------------------------------------------------------------------------------- /scripts/manage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function verbose_eval { 4 | echo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 | echo $* 6 | echo 7 | eval $* 8 | echo --------------------------------------------------------------------- 9 | echo 10 | } 11 | 12 | export PYTHONPATH="src/django-weave/:${PYTHONPATH}" 13 | export DJANGO_SETTINGS_MODULE=testproject.settings 14 | 15 | activate_file="./bin/activate" 16 | 17 | echo _____________________________________________________________________ 18 | echo activate the virtual environment: 19 | 20 | if [ ! -f $activate_file ] 21 | then 22 | echo 23 | echo " **** Error: File '$activate_file' not exists!" 24 | echo 25 | else 26 | verbose_eval source $activate_file 27 | 28 | echo _____________________________________________________________________ 29 | echo execute manage.py 30 | verbose_eval python src/django-weave/testproject/manage.py $* 31 | fi 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | distutils setup 6 | ~~~~~~~~~~~~~~~ 7 | 8 | :copyleft: 2010-2012 by the django-sync-server team, see AUTHORS for more details. 9 | :license: GNU GPL v3 or above, see LICENSE for more details. 10 | """ 11 | 12 | import os 13 | import sys 14 | 15 | from setuptools import setup, find_packages 16 | 17 | from weave import VERSION_STRING 18 | 19 | PACKAGE_ROOT = os.path.dirname(os.path.abspath(__file__)) 20 | 21 | 22 | # convert creole to ReSt on-the-fly, see also: 23 | # https://code.google.com/p/python-creole/wiki/UseInSetup 24 | try: 25 | from creole.setup_utils import get_long_description 26 | except ImportError: 27 | if "register" in sys.argv or "sdist" in sys.argv or "--long-description" in sys.argv: 28 | etype, evalue, etb = sys.exc_info() 29 | evalue = etype("%s - Please install python-creole >= v0.8 - e.g.: pip install python-creole" % evalue) 30 | raise etype, evalue, etb 31 | long_description = None 32 | else: 33 | long_description = get_long_description(PACKAGE_ROOT) 34 | 35 | 36 | def get_authors(): 37 | authors = [] 38 | try: 39 | f = file(os.path.join(PACKAGE_ROOT, "AUTHORS"), "r") 40 | except Exception, err: 41 | return ["[Error reading AUTHORS file: %s]" % err] 42 | for line in f: 43 | if line.startswith('*'): 44 | authors.append(line[1:].strip()) 45 | f.close() 46 | return authors 47 | 48 | 49 | setup( 50 | name='django-sync-server', 51 | version=VERSION_STRING, 52 | description='django-sync-server is a Django reusable application witch implements a Firefox weave server.', 53 | long_description=long_description, 54 | author=get_authors(), 55 | maintainer="Jens Diemer", 56 | maintainer_email="django-sync-server@jensdiemer.de", 57 | url='http://code.google.com/p/django-sync-server/', 58 | packages=find_packages(exclude=['testproject', 'testproject.*']), 59 | include_package_data=True, # include files specified by MANIFEST.in 60 | install_requires=[ 61 | "Django", 62 | "South", 63 | ], 64 | zip_safe=False, 65 | classifiers=[ 66 | # "Development Status :: 1 - Planning", 67 | # "Development Status :: 2 - Pre-Alpha", 68 | # "Development Status :: 3 - Alpha", 69 | # "Development Status :: 4 - Beta", 70 | "Development Status :: 5 - Production/Stable", 71 | "Environment :: Web Environment", 72 | "Intended Audience :: Developers", 73 | # "Intended Audience :: Education", 74 | # "Intended Audience :: End Users/Desktop", 75 | "License :: OSI Approved :: GNU General Public License (GPL)", 76 | "Programming Language :: Python", 77 | 'Framework :: Django', 78 | "Topic :: Database :: Front-Ends", 79 | "Topic :: Documentation", 80 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 81 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 82 | "Operating System :: OS Independent", 83 | ] 84 | ) 85 | -------------------------------------------------------------------------------- /testproject/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Check some external libs with pkg_resources.require() 5 | We only create warnings on VersionConflict and DistributionNotFound exceptions. 6 | 7 | See also: ./scripts/requirements/external_apps.txt 8 | See also: ./scripts/requirements/libs.txt 9 | 10 | Format info for pkg_resources.require(): 11 | http://peak.telecommunity.com/DevCenter/PkgResources#requirement-objects 12 | """ 13 | 14 | 15 | import warnings 16 | try: 17 | import pkg_resources 18 | except ImportError, e: 19 | import sys 20 | etype, evalue, etb = sys.exc_info() 21 | evalue = etype( 22 | ( 23 | "%s - Have you installed setuptools?" 24 | " See: http://pypi.python.org/pypi/setuptools" 25 | " - Or is the virtualenv not activated?" 26 | ) % evalue 27 | ) 28 | raise etype, evalue, etb 29 | 30 | 31 | def check_require(requirements): 32 | """ 33 | Check a package list. 34 | Display only warnings on VersionConflict and DistributionNotFound exceptions. 35 | """ 36 | for requirement in requirements: 37 | try: 38 | pkg_resources.require(requirement) 39 | except pkg_resources.VersionConflict, err: 40 | warnings.warn("Version conflict: %s" % err) 41 | except pkg_resources.DistributionNotFound, err: 42 | warnings.warn("Distribution not found: %s" % err) 43 | 44 | 45 | requirements = ( 46 | # http://code.djangoproject.com/browser/django/trunk/django/__init__.py 47 | "django >= 1.1", 48 | 49 | # http://code.google.com/p/django-tools/source/browse/trunk/django_tools/__init__.py 50 | # "django-tools >= 0.7.0beta", 51 | 52 | # http://code.google.com/p/django-reversion/source/browse/trunk/src/setup.py 53 | # "django-reversion >= 1.1.2", 54 | ) 55 | 56 | 57 | check_require(requirements) 58 | -------------------------------------------------------------------------------- /testproject/logger.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,django_weave 3 | 4 | [handlers] 5 | keys=consoleHandler 6 | 7 | [formatters] 8 | keys=simpleFormatter 9 | 10 | [logger_root] 11 | level=DEBUG 12 | handlers=consoleHandler 13 | 14 | [logger_django_weave] 15 | level=DEBUG 16 | handlers=consoleHandler 17 | qualname=simpleExample 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=DEBUG 23 | formatter=simpleFormatter 24 | args=(sys.stdout,) 25 | 26 | [formatter_simpleFormatter] 27 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 28 | datefmt= -------------------------------------------------------------------------------- /testproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | """ 5 | weave - manage.py 6 | ~~~~~~~~~~~~~~~~~~~ 7 | 8 | http://docs.djangoproject.com/en/dev/ref/django-admin/ 9 | 10 | borrowed from the pinax project. 11 | """ 12 | 13 | import os 14 | import sys 15 | 16 | if 'DJANGO_SETTINGS_MODULE' not in os.environ: 17 | os.environ['DJANGO_SETTINGS_MODULE'] = "testproject.settings" 18 | print "DJANGO_SETTINGS_MODULE not set, use: %r" % os.environ['DJANGO_SETTINGS_MODULE'] 19 | 20 | def _error(msg): 21 | print "Import Error:", msg 22 | print "-" * 79 23 | import traceback 24 | traceback.print_exc() 25 | print "-" * 79 26 | import sys 27 | for p in sys.path: 28 | print p 29 | print "-" * 79 30 | print "Did you activate the virtualenv?" 31 | sys.exit(1) 32 | 33 | try: 34 | from django.core.management import setup_environ, execute_from_command_line 35 | except ImportError, msg: 36 | _error(msg) 37 | 38 | 39 | try: 40 | import weave 41 | except ImportError, msg: 42 | _error(msg) 43 | except: 44 | import traceback 45 | traceback.print_exc() 46 | 47 | 48 | try: 49 | import settings as settings_mod # Assumed to be in the same directory. 50 | except ImportError: 51 | import sys 52 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 53 | sys.exit(1) 54 | except: 55 | import traceback 56 | traceback.print_exc() 57 | 58 | try: 59 | # setup the environment before we start accessing things in the settings. 60 | setup_environ(settings_mod) 61 | except: 62 | import traceback 63 | traceback.print_exc() 64 | 65 | if __name__ == "__main__": 66 | try: 67 | execute_from_command_line() 68 | except Exception: 69 | import traceback 70 | traceback.print_exc() 71 | 72 | -------------------------------------------------------------------------------- /testproject/settings.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | testproject.settings 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | IMPORTANT: 7 | You should not edit this file! 8 | Overwrite settings with a local settings file: 9 | local_settings.py 10 | more info: 11 | http://code.google.com/p/django-sync-server/wiki/WeaveSettings 12 | 13 | Here are not all settings predefined you can use. Please look at the 14 | django documentation for a full list of all items: 15 | http://www.djangoproject.com/documentation/settings/ 16 | 17 | :copyleft: 2010-2011 by the django-sync-server team, see AUTHORS for more details. 18 | :license: GNU GPL v3 or above, see LICENSE for more details. 19 | """ 20 | 21 | import os 22 | import sys 23 | 24 | try: 25 | #from django_tools.utils import info_print;info_print.redirect_stdout() 26 | import django 27 | import weave 28 | except Exception, e: 29 | import traceback 30 | print "-" * 79 31 | sys.stderr.write(traceback.format_exc()) 32 | print "-" * 79 33 | raise 34 | 35 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) 36 | 37 | DEBUG = True 38 | TEMPLATE_DEBUG = DEBUG 39 | 40 | ADMINS = () 41 | 42 | MANAGERS = ADMINS 43 | 44 | DATABASES = { 45 | 'default': { 46 | 'ENGINE': 'django.db.backends.sqlite3', 47 | 'NAME': os.path.join(PROJECT_ROOT, 'test.db3') 48 | } 49 | } 50 | 51 | TIME_ZONE = "UTC" 52 | 53 | LANGUAGE_CODE = 'en-us' 54 | 55 | SITE_ID = 1 56 | 57 | USE_I18N = True 58 | 59 | MEDIA_ROOT = '' 60 | 61 | MEDIA_URL = '' 62 | 63 | ADMIN_MEDIA_PREFIX = '/media/' 64 | 65 | SECRET_KEY = "Make this unique, and don't share it with anybody!" 66 | 67 | MIDDLEWARE_CLASSES = ( 68 | 'django.middleware.common.CommonMiddleware', 69 | 'django.contrib.sessions.middleware.SessionMiddleware', 70 | 'django.middleware.csrf.CsrfViewMiddleware', 71 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 72 | 'django.middleware.transaction.TransactionMiddleware', 73 | ) 74 | 75 | LOGIN_URL = "admin" 76 | 77 | ROOT_URLCONF = 'testproject.urls' 78 | 79 | 80 | TEMPLATE_DIRS = ( 81 | os.path.join(os.path.abspath(os.path.dirname(django.__file__)), "contrib/admin/templates"), 82 | os.path.join(PROJECT_ROOT, "templates"), 83 | ) 84 | 85 | INSTALLED_APPS = ( 86 | 'django.contrib.auth', 87 | 'django.contrib.contenttypes', 88 | 'django.contrib.sessions', 89 | 'django.contrib.sites', 90 | 'django.contrib.admin', 91 | 'django.contrib.admindocs', 92 | "weave", 93 | "south", 94 | ) 95 | 96 | from weave import app_settings as WEAVE 97 | 98 | try: 99 | from local_settings import * 100 | except ImportError, err: 101 | msg = ( 102 | "No local_settings.py imported from '%s' !" 103 | " (Original error was: %s)\n" 104 | ) % (os.getcwd(), err) 105 | sys.stderr.write(msg) 106 | -------------------------------------------------------------------------------- /testproject/templates/testproject/url_info.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block content %} 4 |

5 | Use {{ server_url }} as server url in your weave client preferences. 6 |

7 |

Some test urls:

8 | 18 | {% endblock content %} -------------------------------------------------------------------------------- /testproject/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.conf.urls.defaults import patterns, include, url 4 | 5 | from django.contrib import admin 6 | admin.autodiscover() 7 | 8 | import weave.urls 9 | from testproject.views import url_info 10 | 11 | 12 | handler404 = 'django.views.defaults.page_not_found' 13 | handler500 = 'django.views.defaults.server_error' 14 | 15 | 16 | urlpatterns = patterns('', 17 | url(r'^$', url_info), 18 | url(r'^weave/', include(weave.urls), name="weave-root"), 19 | 20 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 21 | url(r'^admin/', include(admin.site.urls)), 22 | ) 23 | -------------------------------------------------------------------------------- /testproject/views.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | 3 | from django.shortcuts import render_to_response 4 | from django.core.urlresolvers import reverse 5 | from django.contrib.auth.decorators import login_required 6 | 7 | 8 | def absolute_uri(request, view_name, **kwargs): 9 | url = reverse(view_name, kwargs=kwargs) 10 | absolute_uri = request.build_absolute_uri(url) 11 | return absolute_uri 12 | 13 | 14 | @login_required 15 | def url_info(request): 16 | 17 | server_url = request.build_absolute_uri("weave") 18 | if not server_url.endswith("/"): 19 | # sync setup dialog only accept the server url if it's ends with a slash 20 | server_url += "/" 21 | 22 | context = { 23 | "title": "weave testproject url info", 24 | "server_url": server_url, 25 | "register_check_url": absolute_uri(request, "weave-register_check", username=request.user.username), 26 | "info_url": absolute_uri(request, "weave-info", version="1.0", username=request.user.username), 27 | } 28 | return render_to_response("testproject/url_info.html", context) 29 | -------------------------------------------------------------------------------- /testproject/weave_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | weave_server.py [options] 3 | 4 | This is a simple reference implementation for a Weave server, 5 | which can also be used to test the Weave client against. 6 | 7 | This server behaves like a standard Weave server, with the 8 | following limitations and/or enhancements: 9 | 10 | * Responses to CAPTCHA challenges during new user registration 11 | are always accepted, with the exception that the magic word 12 | 'bad' always fails (this was added for testing purposes). 13 | 14 | * Validation emails are not sent, though the server tells 15 | clients that they are. 16 | 17 | * The url at '/state/' can be accessed to retrieve a snapshot of 18 | the current state of the server. This data can be saved to a 19 | file and restored later with the '--state' command-line 20 | option. The state is also just a Pythonic representation of 21 | the server's state, and as such is relatively human-readable 22 | and can be hand-edited if necessary. 23 | 24 | * Timeout values sent with HTTP LOCK requests are ignored. 25 | 26 | * The server operates over HTTP, not HTTPS. 27 | """ 28 | 29 | from wsgiref.simple_server import make_server 30 | from optparse import OptionParser 31 | import httplib 32 | import base64 33 | import logging 34 | import pprint 35 | import cgi 36 | 37 | DEFAULT_PORT = 8000 38 | DEFAULT_REALM = "services.mozilla.com" 39 | CAPTCHA_FAILURE_MAGIC_WORD = "bad" 40 | 41 | class HttpResponse(object): 42 | def __init__(self, code, content = "", content_type = "text/plain"): 43 | self.status = "%s %s" % (code, httplib.responses.get(code, "")) 44 | self.headers = [("Content-type", content_type)] 45 | if code == httplib.UNAUTHORIZED: 46 | self.headers += [("WWW-Authenticate", 47 | "Basic realm=\"%s\"" % DEFAULT_REALM)] 48 | if not content: 49 | content = self.status 50 | self.content = content 51 | 52 | class HttpRequest(object): 53 | def __init__(self, environ): 54 | self.environ = environ 55 | content_length = environ.get("CONTENT_LENGTH") 56 | if content_length: 57 | stream = environ["wsgi.input"] 58 | self.contents = stream.read(int(content_length)) 59 | else: 60 | self.contents = "" 61 | 62 | class Perms(object): 63 | # Special identifier to indicate 'everyone' instead of a 64 | # particular user. 65 | EVERYONE = 0 66 | 67 | def __init__(self, readers=None, writers=None): 68 | if not readers: 69 | readers = [] 70 | if not writers: 71 | writers = [] 72 | 73 | self.readers = readers 74 | self.writers = writers 75 | 76 | def __is_privileged(self, user, access_list): 77 | return (user in access_list or self.EVERYONE in access_list) 78 | 79 | def can_read(self, user): 80 | return self.__is_privileged(user, self.readers) 81 | 82 | def can_write(self, user): 83 | return self.__is_privileged(user, self.writers) 84 | 85 | def __acl_repr(self, acl): 86 | items = [] 87 | for item in acl: 88 | if item == self.EVERYONE: 89 | items.append("Perms.EVERYONE") 90 | else: 91 | items.append(repr(item)) 92 | return "[" + ", ".join(items) + "]" 93 | 94 | def __repr__(self): 95 | return "Perms(readers=%s, writers=%s)" % ( 96 | self.__acl_repr(self.readers), 97 | self.__acl_repr(self.writers) 98 | ) 99 | 100 | def requires_read_access(function): 101 | function._requires_read_access = True 102 | return function 103 | 104 | def requires_write_access(function): 105 | function._requires_write_access = True 106 | return function 107 | 108 | class WeaveApp(object): 109 | """ 110 | WSGI app for the Weave server. 111 | """ 112 | 113 | __CAPTCHA_HTML = '\n\n\t' 114 | 115 | def __init__(self, state=None): 116 | self.contents = {} 117 | self.dir_perms = {"/" : Perms(readers=[Perms.EVERYONE])} 118 | self.passwords = {} 119 | self.email = {} 120 | self.locks = {} 121 | self._tokenIds = 0 122 | 123 | if state: 124 | self.__setstate__(state) 125 | 126 | def add_user(self, username, password, email = None): 127 | assert username, "Username cannot be empty" 128 | assert password, "Password cannot be empty" 129 | 130 | home_dir = "/user/%s/" % username 131 | public_dir = home_dir + "public/" 132 | self.dir_perms[home_dir] = Perms(readers=[username], 133 | writers=[username]) 134 | self.dir_perms[public_dir] = Perms(readers=[Perms.EVERYONE], 135 | writers=[username]) 136 | self.passwords[username] = password 137 | if email: 138 | self.email[email] = username 139 | 140 | def __get_perms_for_path(self, path): 141 | possible_perms = [dirname for dirname in self.dir_perms 142 | if path.startswith(dirname)] 143 | possible_perms.sort(key = len) 144 | perms = possible_perms[-1] 145 | return self.dir_perms[perms] 146 | 147 | def __get_files_in_dir(self, path): 148 | return [filename for filename in self.contents 149 | if filename.startswith(path)] 150 | 151 | def __api_share(self, path): 152 | params = cgi.parse_qs(self.request.contents) 153 | user = params["uid"][0] 154 | password = params["password"][0] 155 | if self.passwords.get(user) != password: 156 | return HttpResponse(httplib.UNAUTHORIZED) 157 | else: 158 | import json 159 | cmd = json.read(params["cmd"][0]) 160 | dirname = "/user/%s/%s" % (user, cmd["directory"]) 161 | if not dirname.endswith("/"): 162 | dirname += "/" 163 | readers = [] 164 | for reader in cmd["share_to_users"]: 165 | if reader == "all": 166 | readers.append(Perms.EVERYONE) 167 | else: 168 | readers.append(reader) 169 | if user not in readers: 170 | readers.append(user) 171 | self.dir_perms[dirname] = Perms(readers = readers, 172 | writers = [user]) 173 | return HttpResponse(httplib.OK, "OK") 174 | 175 | # Registration API 176 | def __api_register_check(self, what, where): 177 | what = what.strip("/") 178 | if what.strip() == "": 179 | return HttpResponse(400, 180 | self.ERR_WRONG_HTTP_METHOD) 181 | 182 | if what in where: 183 | return HttpResponse(httplib.OK, 184 | self.ERR_UID_OR_EMAIL_IN_USE) 185 | else: 186 | return HttpResponse(httplib.OK, 187 | self.ERR_UID_OR_EMAIL_AVAILABLE) 188 | 189 | ERR_UID_OR_EMAIL_AVAILABLE = "1" 190 | ERR_WRONG_HTTP_METHOD = "-1" 191 | ERR_MISSING_UID = "-2" 192 | ERR_INVALID_UID = "-3" 193 | ERR_UID_OR_EMAIL_IN_USE = "0" 194 | ERR_EMAIL_IN_USE = "-5" 195 | ERR_MISSING_PASSWORD = "-8" 196 | ERR_MISSING_RECAPTCHA_CHALLENGE_FIELD = "-6" 197 | ERR_MISSING_RECAPTCHA_RESPONSE_FIELD = "-7" 198 | ERR_MISSING_NEW = "-11" 199 | ERR_INCORRECT_PASSWORD = "-12" 200 | ERR_ACCOUNT_CREATED_VERIFICATION_SENT = "2" 201 | ERR_ACCOUNT_CREATED = "3" 202 | 203 | __REQUIRED_CHANGE_PASSWORD_FIELDS = ["uid", "password", "new"] 204 | 205 | __REQUIRED_NEW_ACCOUNT_FIELDS = ["uid", 206 | "password", 207 | "recaptcha_challenge_field", 208 | "recaptcha_response_field"] 209 | 210 | __FIELD_ERRORS = { 211 | "uid" : ERR_MISSING_UID, 212 | "password" : ERR_MISSING_PASSWORD, 213 | "new" : ERR_MISSING_NEW, 214 | "recaptcha_challenge_field" : ERR_MISSING_RECAPTCHA_CHALLENGE_FIELD, 215 | "recaptcha_response_field" : ERR_MISSING_RECAPTCHA_RESPONSE_FIELD 216 | } 217 | 218 | def __get_fields(self, required_fields): 219 | params = cgi.parse_qs(self.request.contents) 220 | fields = {} 221 | for name in params: 222 | fields[name] = params[name][0] 223 | for name in required_fields: 224 | if not fields.get(name): 225 | return HttpResponse(httplib.BAD_REQUEST, 226 | self.__FIELD_ERRORS[name]) 227 | return fields 228 | 229 | def __api_create_account(self, path): 230 | fields = self.__get_fields(self.__REQUIRED_NEW_ACCOUNT_FIELDS) 231 | if isinstance(fields, HttpResponse): 232 | return fields 233 | if fields["uid"] in self.passwords: 234 | return HttpResponse(httplib.BAD_REQUEST, 235 | self.ERR_UID_OR_EMAIL_IN_USE) 236 | if fields["recaptcha_response_field"] == CAPTCHA_FAILURE_MAGIC_WORD: 237 | return HttpResponse(httplib.EXPECTATION_FAILED) 238 | if fields.get("mail"): 239 | if self.email.get(fields["mail"]): 240 | return HttpResponse(httplib.BAD_REQUEST, 241 | self.ERR_EMAIL_IN_USE) 242 | # TODO: We're not actually sending an email... 243 | body_code = self.ERR_ACCOUNT_CREATED_VERIFICATION_SENT 244 | else: 245 | body_code = self.ERR_ACCOUNT_CREATED 246 | 247 | self.add_user(fields["uid"], fields["password"], 248 | fields.get("mail")) 249 | return HttpResponse(httplib.CREATED, body_code) 250 | 251 | def __api_change_password(self, path): 252 | fields = self.__get_fields(self.__REQUIRED_CHANGE_PASSWORD_FIELDS) 253 | if isinstance(fields, HttpResponse): 254 | return fields 255 | if not self.passwords.get(fields["uid"]): 256 | return HttpResponse(httplib.BAD_REQUEST, 257 | self.ERR_INVALID_UID) 258 | if self.passwords[fields["uid"]] != fields["password"]: 259 | return HttpResponse(httplib.BAD_REQUEST, 260 | self.ERR_INCORRECT_PASSWORD) 261 | self.passwords[fields["uid"]] = fields["new"] 262 | return HttpResponse(httplib.OK) 263 | 264 | # HTTP method handlers 265 | 266 | @requires_write_access 267 | def _handle_LOCK(self, path): 268 | if path in self.locks: 269 | return HttpResponse(httplib.LOCKED) 270 | token = "opaquelocktoken:%d" % self._tokenIds 271 | self._tokenIds += 1 272 | self.locks[path] = token 273 | response = """ 274 | 275 | 276 | 277 | 278 | %s 279 | 280 | 281 | 282 | """ % token 283 | return HttpResponse(httplib.OK, response, content_type="text/xml") 284 | 285 | @requires_write_access 286 | def _handle_UNLOCK(self, path): 287 | token = self.request.environ["HTTP_LOCK_TOKEN"] 288 | if path not in self.locks: 289 | return HttpResponse(httplib.BAD_REQUEST) 290 | if token == "<%s>" % self.locks[path]: 291 | del self.locks[path] 292 | return HttpResponse(httplib.NO_CONTENT) 293 | return HttpResponse(httplib.BAD_REQUEST) 294 | 295 | @requires_write_access 296 | def _handle_MKCOL(self, path): 297 | return HttpResponse(httplib.OK) 298 | 299 | @requires_write_access 300 | def _handle_PUT(self, path): 301 | self.contents[path] = self.request.contents 302 | return HttpResponse(httplib.OK) 303 | 304 | def _handle_POST(self, path): 305 | if path == "/api/share/": 306 | return self.__api_share(path) 307 | elif path == "/api/register/new/": 308 | return self.__api_create_account(path) 309 | elif path == "/api/register/chpwd/": 310 | return self.__api_change_password(path) 311 | else: 312 | return HttpResponse(httplib.NOT_FOUND) 313 | 314 | @requires_write_access 315 | def _handle_PROPFIND(self, path): 316 | response = """ 317 | """ 318 | 319 | path_template = """ 320 | %(href)s 321 | 322 | 323 | %(props)s 324 | 325 | HTTP/1.1 200 OK 326 | 327 | """ 328 | 329 | if path in self.locks: 330 | props = "%s" % ( 331 | self.locks[path] 332 | ) 333 | else: 334 | props = "" 335 | 336 | response += path_template % {"href": path, 337 | "props": props} 338 | 339 | if path.endswith("/"): 340 | for filename in self.__get_files_in_dir(path): 341 | response += path_template % {"href" : filename, 342 | "props" : ""} 343 | 344 | response += """""" 345 | return HttpResponse(httplib.MULTI_STATUS, response, 346 | content_type="text/xml") 347 | 348 | @requires_write_access 349 | def _handle_DELETE(self, path): 350 | response = HttpResponse(httplib.OK) 351 | if path.endswith("/"): 352 | # Delete a directory. 353 | for filename in self.__get_files_in_dir(path): 354 | del self.contents[filename] 355 | else: 356 | # Delete a file. 357 | if path not in self.contents: 358 | response = HttpResponse(httplib.NOT_FOUND) 359 | else: 360 | del self.contents[path] 361 | return response 362 | 363 | @requires_read_access 364 | def _handle_GET(self, path): 365 | if path in self.contents: 366 | return HttpResponse(httplib.OK, self.contents[path]) 367 | elif path == "/state/": 368 | state_str = pprint.pformat(self.__getstate__()) 369 | return HttpResponse(httplib.OK, state_str) 370 | elif path == "/api/register/new/": 371 | return HttpResponse(httplib.OK, self.__CAPTCHA_HTML, 372 | content_type = "text/html") 373 | elif path.startswith("/api/register/check/"): 374 | return self.__api_register_check(path[20:], self.passwords) 375 | elif path.startswith("/api/register/chkmail/"): 376 | return self.__api_register_check(path[22:], self.email) 377 | elif path.endswith("/"): 378 | return self.__show_index(path) 379 | else: 380 | return HttpResponse(httplib.NOT_FOUND) 381 | 382 | def __getstate__(self): 383 | state = {} 384 | state.update(self.__dict__) 385 | del state["request"] 386 | return state 387 | 388 | def __setstate__(self, state): 389 | self.__dict__.update(state) 390 | 391 | def __show_index(self, path): 392 | output = [] 393 | for filename in self.__get_files_in_dir(path): 394 | output.append("

%s

" % (filename, 395 | filename)) 396 | if output: 397 | output = "".join(output) 398 | else: 399 | output = ("

There are no files under the " 400 | "directory %s.

" % (path)) 401 | return HttpResponse(httplib.OK, output, content_type="text/html") 402 | 403 | def __process_handler(self, handler): 404 | response = None 405 | auth = self.request.environ.get("HTTP_AUTHORIZATION") 406 | if auth: 407 | user, password = base64.b64decode(auth.split()[1]).split(":") 408 | if self.passwords.get(user) != password: 409 | response = HttpResponse(httplib.UNAUTHORIZED) 410 | else: 411 | user = Perms.EVERYONE 412 | 413 | if response is None: 414 | path = self.request.environ["PATH_INFO"] 415 | perms = self.__get_perms_for_path(path) 416 | checks = [] 417 | if hasattr(handler, "_requires_read_access"): 418 | checks.append(perms.can_read) 419 | if hasattr(handler, "_requires_write_access"): 420 | checks.append(perms.can_write) 421 | for check in checks: 422 | if not check(user): 423 | response = HttpResponse(httplib.UNAUTHORIZED) 424 | 425 | if response is None: 426 | response = handler(path) 427 | 428 | return response 429 | 430 | def __call__(self, environ, start_response): 431 | """ 432 | Main WSGI application method. 433 | """ 434 | 435 | self.request = HttpRequest(environ) 436 | method = "_handle_%s" % environ["REQUEST_METHOD"] 437 | 438 | # See if we have a method called 'handle_', where 439 | # is the name of the HTTP method to call. If we do, 440 | # then call it. 441 | if hasattr(self, method): 442 | handler = getattr(self, method) 443 | response = self.__process_handler(handler) 444 | else: 445 | response = HttpResponse( 446 | httplib.METHOD_NOT_ALLOWED, 447 | "Method %s is not yet implemented." % method 448 | ) 449 | 450 | start_response(response.status, response.headers) 451 | return [response.content] 452 | 453 | if __name__ == "__main__": 454 | usage = __import__("__main__").__doc__ 455 | parser = OptionParser(usage = usage) 456 | parser.add_option("-s", "--state", dest="state_filename", 457 | help="retrieve server state from filename") 458 | options, args = parser.parse_args() 459 | 460 | print "Weave Development Server" 461 | print 462 | print "Run this script with '-h' for usage information." 463 | 464 | logging.basicConfig(level=logging.DEBUG) 465 | 466 | if options.state_filename: 467 | filename = options.state_filename 468 | logging.info("Setting initial state from '%s'." % filename) 469 | data = open(filename, "r").read() 470 | state = eval(data) 471 | app = WeaveApp(state) 472 | else: 473 | app = WeaveApp() 474 | 475 | logging.info("Serving on port %d." % DEFAULT_PORT) 476 | httpd = make_server('', DEFAULT_PORT, app) 477 | httpd.serve_forever() 478 | -------------------------------------------------------------------------------- /testproject/weave_server_tester.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple script to test Weave server support and ensure that it 3 | works properly. 4 | """ 5 | 6 | import sys 7 | import urllib 8 | import urllib2 9 | import httplib 10 | from urlparse import urlsplit 11 | from xml.etree import cElementTree as ET 12 | 13 | import json 14 | import weave_server 15 | import threading 16 | 17 | class DavRequest(urllib2.Request): 18 | def __init__(self, method, *args, **kwargs): 19 | urllib2.Request.__init__(self, *args, **kwargs) 20 | self.__method = method 21 | 22 | def get_method(self): 23 | return self.__method 24 | 25 | class DavHandler(urllib2.BaseHandler): 26 | def _normal_response(self, req, fp, code, msg, headers): 27 | return fp 28 | 29 | # Multi-status 30 | http_error_207 = _normal_response 31 | 32 | # Created 33 | http_error_201 = _normal_response 34 | 35 | # Accepted 36 | http_error_202 = _normal_response 37 | 38 | # No content 39 | http_error_204 = _normal_response 40 | 41 | class WeaveSession(object): 42 | def __init__(self, username, password, server_url, 43 | realm=weave_server.DEFAULT_REALM): 44 | self.username = username 45 | self.server_url = server_url 46 | self.server = urlsplit(server_url).netloc 47 | self.realm = realm 48 | self.password = password 49 | 50 | def clone(self): 51 | return WeaveSession(self.username, self.password, 52 | self.server_url, self.realm) 53 | 54 | def _open(self, req): 55 | davHandler = DavHandler() 56 | authHandler = urllib2.HTTPBasicAuthHandler() 57 | authHandler.add_password(self.realm, 58 | self.server, 59 | self.username, 60 | self.password) 61 | opener = urllib2.build_opener(authHandler, davHandler) 62 | if isinstance(req, urllib2.Request): 63 | print req.get_data() 64 | print req.get_full_url() 65 | else: 66 | print req 67 | return opener.open(req) 68 | 69 | def _get_user_url(self, path, user=None): 70 | if not user: 71 | user = self.username 72 | if path.startswith("/"): 73 | path = path[1:] 74 | url = "%s/user/%s/%s" % (self.server_url, 75 | user, 76 | path) 77 | return url 78 | 79 | def list_files(self, path): 80 | xml_data = ("" 81 | "") 82 | 83 | url = self._get_user_url(path) 84 | headers = {"Content-type" : "text/xml; charset=\"utf-8\"", 85 | "Depth" : "1"} 86 | req = DavRequest("PROPFIND", url, xml_data, headers=headers) 87 | result_xml = self._open(req).read() 88 | 89 | multistatus = ET.XML(result_xml) 90 | hrefs = multistatus.findall(".//{DAV:}href") 91 | root = hrefs[0].text 92 | return [href.text[len(root):] for href in hrefs[1:]] 93 | 94 | def create_dir(self, path): 95 | req = DavRequest("MKCOL", self._get_user_url(path)) 96 | self._open(req) 97 | 98 | def remove_dir(self, path): 99 | if not path[-1] == "/": 100 | path += "/" 101 | self.delete_file(path) 102 | 103 | def get_file(self, path, user=None): 104 | obj = self._open(self._get_user_url(path, user)) 105 | return obj.read() 106 | 107 | def put_file(self, path, data): 108 | req = DavRequest("PUT", self._get_user_url(path), data) 109 | self._open(req) 110 | 111 | def delete_file(self, path): 112 | req = DavRequest("DELETE", self._get_user_url(path)) 113 | self._open(req) 114 | 115 | def lock_file(self, path): 116 | headers = {"Content-type" : "text/xml; charset=\"utf-8\"", 117 | "Depth" : "infinity", 118 | "Timeout": "Second-600"} 119 | xml_data = ("\n" 120 | "\n" 121 | " \n" 122 | " \n" 123 | "") 124 | req = DavRequest("LOCK", self._get_user_url(path), xml_data, 125 | headers=headers) 126 | result_xml = self._open(req).read() 127 | 128 | response = ET.XML(result_xml) 129 | token = response.find(".//{DAV:}locktoken/{DAV:}href").text 130 | return token 131 | 132 | def unlock_file(self, path, token): 133 | headers = {"Lock-Token" : "<%s>" % token} 134 | req = DavRequest("UNLOCK", self._get_user_url(path), 135 | headers=headers) 136 | self._open(req) 137 | 138 | def ensure_unlock_file(self, path): 139 | xml_data = ("" 140 | "" 141 | "") 142 | 143 | url = self._get_user_url(path) 144 | headers = {"Content-type" : "text/xml; charset=\"utf-8\"", 145 | "Depth" : "0"} 146 | req = DavRequest("PROPFIND", url, xml_data, headers=headers) 147 | try: 148 | result_xml = self._open(req).read() 149 | except urllib2.HTTPError, e: 150 | return 151 | 152 | multistatus = ET.XML(result_xml) 153 | href = multistatus.find(".//{DAV:}locktoken/{DAV:}href") 154 | if href is not None: 155 | self.unlock_file(path, href.text) 156 | 157 | def does_email_exist(self, email): 158 | return self._does_entity_exist("chkmail", email) 159 | 160 | def does_username_exist(self, username): 161 | return self._does_entity_exist("check", username) 162 | 163 | def _does_entity_exist(self, entity_kind, entity): 164 | url = "%s/api/register/%s/%s" % (self.server_url, 165 | entity_kind, 166 | entity) 167 | result = int(self._open(url).read()) 168 | print "result: %r" % result 169 | if result == 0: 170 | return True 171 | elif result == 1: 172 | return False 173 | else: 174 | raise Exception("Unexpected result code: %d" % result) 175 | 176 | def change_password(self, new_password): 177 | url = "%s/api/register/chpwd/" % (self.server_url) 178 | postdata = urllib.urlencode({"uid" : self.username, 179 | "password" : self.password, 180 | "new" : new_password}) 181 | req = urllib2.Request(url, postdata) 182 | print "change password reponse: %r" % self._open(req).read() 183 | self.password = new_password 184 | 185 | def share_with_users(self, path, users): 186 | url = "%s/api/share/" % (self.server_url) 187 | cmd = {"version" : 1, 188 | "directory" : path, 189 | "share_to_users" : users} 190 | postdata = urllib.urlencode({"cmd" : json.write(cmd), 191 | "uid" : self.username, 192 | "password" : self.password}) 193 | req = urllib2.Request(url, postdata) 194 | result = self._open(req).read() 195 | if result != "OK": 196 | raise Exception("Share attempt failed: %s" % result) 197 | 198 | def ensure_weave_disallows_php(session): 199 | print "Ensuring that weave disallows PHP upload and execution." 200 | session.put_file("phptest.php", "") 201 | try: 202 | if session.get_file("phptest.php") == "hai2u!": 203 | raise Exception("Weave server allows PHP execution!") 204 | finally: 205 | session.delete_file("phptest.php") 206 | 207 | def _do_test(session_1, session_2): 208 | print "Ensuring that user '%s' exists." % session_1.username 209 | assert session_1.does_username_exist(session_1.username) 210 | 211 | print "Ensuring that user '%s' exists." % session_2.username 212 | assert session_1.does_username_exist(session_2.username) 213 | 214 | print "Changing password of user '%s' to 'blarg'." % session_1.username 215 | old_pwd = session_1.password 216 | old_session = session_1.clone() 217 | try: 218 | session_1.change_password("blarg") 219 | except urllib2.HTTPError, e: 220 | if (e.code == httplib.BAD_REQUEST and 221 | e.read() == weave_server.WeaveApp.ERR_INCORRECT_PASSWORD): 222 | print ("That didn't work; an old run of this test may " 223 | "have been aborted. Trying to revert...") 224 | session_1.password = "blarg" 225 | session_1.change_password(old_pwd) 226 | print "Revert successful, attempting to change password again." 227 | session_1.change_password("blarg") 228 | else: 229 | raise 230 | 231 | try: 232 | print "Ensuring we can't log in using old password." 233 | old_session.change_password("fnarg") 234 | except urllib2.HTTPError, e: 235 | if e.code != httplib.BAD_REQUEST: 236 | raise 237 | content = e.read() 238 | print "Content: %r" % content 239 | if content != weave_server.WeaveApp.ERR_INCORRECT_PASSWORD: 240 | raise AssertionError("Bad return value: %s" % content) 241 | else: 242 | raise AssertionError("We could log in using the old password!") 243 | 244 | print "Reverting back to old password." 245 | session_1.change_password(old_pwd) 246 | 247 | print 248 | print "*" * 79 249 | print 250 | 251 | print "Ensuring that file is not locked." 252 | session_1.ensure_unlock_file("test_lock") 253 | 254 | print "Locking file" 255 | session_1.lock_file("test_lock") 256 | 257 | print "Unlocking file by querying for its token" 258 | session_1.ensure_unlock_file("test_lock") 259 | 260 | print "Locking file again" 261 | token = session_1.lock_file("test_lock") 262 | 263 | try: 264 | print "Ensuring that we can't re-lock the file." 265 | session_1.lock_file("test_lock") 266 | except urllib2.HTTPError, e: 267 | if e.code != httplib.LOCKED: 268 | raise 269 | else: 270 | raise AssertionError("We can re-lock the file!") 271 | 272 | print "Unlocking file" 273 | session_1.unlock_file("test_lock", token) 274 | 275 | # FIXME 276 | # print "Ensuring that PROPFIND on the user's home dir works." 277 | # files = session_1.list_files("") 278 | 279 | # print "Cleaning up any files left over from a failed previous test." 280 | # if "blargle/bloop" in files: 281 | # session_1.delete_file("blargle/bloop") 282 | # if "blargle/" in files: 283 | # session_1.remove_dir("blargle") 284 | 285 | print "Creating directory." 286 | session_1.create_dir("blargle") 287 | 288 | print "Ensuring that directory indexes don't raise errors." 289 | session_1.get_file("") 290 | 291 | try: 292 | print "Creating temporary file." 293 | session_1.put_file("blargle/bloop", "hai2u!") 294 | print "Verifying that temporary file is listed." 295 | assert "bloop" in session_1.list_files("blargle/") 296 | try: 297 | assert session_1.get_file("blargle/bloop") == "hai2u!" 298 | session_1.share_with_users("blargle", []) 299 | try: 300 | print "Ensuring user 2 can't read user 1's file." 301 | session_2.get_file("blargle/bloop", session_1.username) 302 | except urllib2.HTTPError, e: 303 | if e.code != httplib.UNAUTHORIZED: 304 | raise 305 | else: 306 | raise AssertionError("User 2 can read user 1's file!") 307 | print "Sharing directory with user 2." 308 | session_1.share_with_users("blargle", [session_2.username]) 309 | print "Ensuring user 2 can read user 1's file." 310 | assert session_2.get_file("blargle/bloop", 311 | session_1.username) == "hai2u!" 312 | print "Sharing directory with everyone." 313 | session_1.share_with_users("blargle", ["all"]) 314 | print "Ensuring user 2 can read user 1's file." 315 | assert session_2.get_file("blargle/bloop", 316 | session_1.username) == "hai2u!" 317 | finally: 318 | session_1.delete_file("blargle/bloop") 319 | finally: 320 | print "Removing directory." 321 | session_1.remove_dir("blargle") 322 | 323 | ensure_weave_disallows_php(session_1) 324 | 325 | print "Test complete." 326 | 327 | def redirect_stdio(func): 328 | def wrapper(*args, **kwargs): 329 | from cStringIO import StringIO 330 | old_stdio = [sys.stdout, sys.stderr] 331 | stream = StringIO() 332 | sys.stderr = sys.stdout = stream 333 | try: 334 | try: 335 | return func(*args, **kwargs) 336 | except Exception, e: 337 | import traceback 338 | traceback.print_exc() 339 | raise Exception("Test failed:\n\n%s" % stream.getvalue()) 340 | finally: 341 | sys.stderr, sys.stdout = old_stdio 342 | 343 | wrapper.__name__ = func.__name__ 344 | return wrapper 345 | 346 | @redirect_stdio 347 | def test_weave_server(): 348 | server_url = "http://127.0.0.1:%d" % weave_server.DEFAULT_PORT 349 | username_1 = "foo" 350 | password_1 = "test123" 351 | username_2 = "bar" 352 | password_2 = "test1234" 353 | 354 | start_event = threading.Event() 355 | 356 | def run_server(): 357 | app = weave_server.WeaveApp() 358 | app.add_user(username_1, password_1) 359 | app.add_user(username_2, password_2) 360 | httpd = weave_server.make_server('', weave_server.DEFAULT_PORT, app) 361 | start_event.set() 362 | while 1: 363 | request, client_address = httpd.socket.accept() 364 | httpd.process_request(request, client_address) 365 | 366 | thread = threading.Thread(target=run_server) 367 | thread.setDaemon(True) 368 | thread.start() 369 | 370 | start_event.wait() 371 | 372 | session_1 = WeaveSession(username_1, password_1, server_url) 373 | session_2 = WeaveSession(username_2, password_2, server_url) 374 | 375 | _do_test(session_1, session_2) 376 | 377 | if __name__ == "__main__": 378 | args = sys.argv[1:] 379 | if len(args) < 5: 380 | print ("usage: %s " 381 | " " % sys.argv[0]) 382 | sys.exit(1) 383 | 384 | server_url = args[0] 385 | username_1 = args[1] 386 | password_1 = args[2] 387 | username_2 = args[3] 388 | password_2 = args[4] 389 | session_1 = WeaveSession(username_1, password_1, server_url) 390 | session_2 = WeaveSession(username_2, password_2, server_url) 391 | 392 | _do_test(session_1, session_2) 393 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 4 | 5 | function verbose_eval { 6 | echo 7 | echo _____________________________________________________________________ 8 | echo $* 9 | echo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 10 | eval $* 11 | echo --------------------------------------------------------------------- 12 | echo 13 | } 14 | 15 | if [ -e .git ]; 16 | then 17 | echo ".git directory found" 18 | verbose_eval git pull origin master 19 | elif [ -e .svn ]; 20 | then 21 | echo ".svn directory found" 22 | verbose_eval svn update 23 | else 24 | echo "no .git or .svn directory found. Can't update" 25 | fi -------------------------------------------------------------------------------- /weave/__init__.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | -Logger helper class 5 | -build version string 6 | 7 | @license: GNU GPL v3 or above, see LICENSE for more details. 8 | @copyleft: 2010-2012 by the django-sync-server team, see AUTHORS for more details. 9 | """ 10 | 11 | 12 | import logging 13 | import os 14 | import subprocess 15 | import time 16 | import warnings 17 | 18 | 19 | __version__ = (0, 4, 2) 20 | __api__ = (1, 1) 21 | 22 | 23 | VERSION_STRING = '.'.join(str(part) for part in __version__) 24 | API_STRING = '.'.join(str(integer) for integer in __api__) 25 | 26 | 27 | 28 | class Logging(object): 29 | """ 30 | A private class that loads and caches some global objects. 31 | """ 32 | logger = None 33 | 34 | def get_logger(cls): 35 | """ 36 | Initializes and returns our logger instance. 37 | """ 38 | if cls.logger is None: 39 | class NullHandler(logging.Handler): 40 | def emit(self, record): 41 | pass 42 | 43 | cls.logger = logging.getLogger('django_weave') 44 | cls.logger.addHandler(NullHandler()) 45 | cls.logger.setLevel(logging.DEBUG) 46 | 47 | return cls.logger 48 | get_logger = classmethod(get_logger) 49 | 50 | 51 | if __name__ == "__main__": 52 | print VERSION_STRING 53 | -------------------------------------------------------------------------------- /weave/admin.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Register all weave models in Django admin interface. 3 | 4 | Created on 15.03.2010 5 | 6 | @license: GNU GPL v3 or above, see LICENSE for more details. 7 | @copyright: 2010 see AUTHORS for more details. 8 | @author: Jens Diemer 9 | @author: Michael Fladischer 10 | ''' 11 | 12 | from django.contrib import admin 13 | # TODO: Find a way to incorporate django-reversion 14 | #from reversion.admin import VersionAdmin 15 | 16 | from weave.models import Wbo, Collection 17 | 18 | class WboAdminInline(admin.TabularInline): 19 | model = Wbo 20 | 21 | class WboAdmin(admin.ModelAdmin): 22 | def payload_cutout(self, obj): 23 | MAX = 100 24 | payload = obj.payload 25 | if len(payload) > MAX: 26 | payload = payload[:MAX] + "..." 27 | return payload 28 | payload_cutout.short_description = "Payload cutout" 29 | list_display = ['id', "user", "wboid", 'collection', "parentid", "modified", "sortindex", "payload_cutout"] 30 | list_filter = ['user', 'collection'] 31 | date_hierarchy = 'modified' 32 | search_fields = ("wboid", "parentid", "sortindex", "payload") 33 | 34 | admin.site.register(Wbo, WboAdmin) 35 | 36 | class CollectionAdmin(admin.ModelAdmin): 37 | list_display = ['id', 'name', 'user', 'modified', 'site'] 38 | list_filter = ['user'] 39 | date_hierarchy = 'modified' 40 | inlines = [WboAdminInline] 41 | 42 | admin.site.register(Collection, CollectionAdmin) 43 | -------------------------------------------------------------------------------- /weave/app_settings.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | django-sync-server app settings 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | All own settings for the django-sync-server app. 7 | 8 | **IMPORTANT:** 9 | You should not edit this file! 10 | Overwrite settings with a local settings file: 11 | local_settings.py 12 | more info: 13 | http://code.google.com/p/django-sync-server/wiki/WeaveSettings 14 | 15 | :copyleft: 2010-2011 by the django-sync-server team, see AUTHORS for more details. 16 | :license: GNU GPL v3 or above, see LICENSE for more details. 17 | """ 18 | 19 | 20 | # Must be obtained from http://recaptcha.net/ by registering an account. 21 | RECAPTCHA_PUBLIC_KEY = '' 22 | RECAPTCHA_PRIVATE_KEY = '' 23 | 24 | # Create users without any captcha. 25 | # NOT RECOMMENDED! Spam bots can flooding your server! 26 | DONT_USE_CAPTCHA = False 27 | 28 | BASICAUTH_REALM = "django-sync-server" 29 | 30 | # Disable own basicauth login? 31 | # If True: basicauth would be deactivated and every login request over 32 | # django-sync-server own views would be denied. 33 | # The user must login in a other way, before using firefox-sync 34 | # e.g. use the django admin login page. 35 | DISABLE_LOGIN = False 36 | 37 | # Log request/reponse debug information 38 | DEBUG_REQUEST = False 39 | -------------------------------------------------------------------------------- /weave/constants.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | from: 5 | https://wiki.mozilla.org/Labs/Weave/ServerAPI#Body_numeric_codes_and_their_meanings 6 | see also: 7 | http://hg.mozilla.org/labs/weave/file/tip/tools/scripts/weave_server.py#l189 8 | 9 | Created on 15.03.2010 10 | 11 | @license: GNU GPL v3 or above, see LICENSE for more details. 12 | @copyright: 2010 see AUTHORS for more details. 13 | @author: Jens Diemer 14 | @author: Michael Fladischer 15 | """ 16 | 17 | ERR_UID_OR_EMAIL_AVAILABLE = "1" 18 | ERR_WRONG_HTTP_METHOD = "-1" 19 | ERR_MISSING_UID = "-2" 20 | ERR_INVALID_UID = "-3" 21 | ERR_UID_OR_EMAIL_IN_USE = "0" 22 | ERR_EMAIL_IN_USE = "-5" 23 | ERR_MISSING_PASSWORD = "-8" 24 | ERR_MISSING_RECAPTCHA_CHALLENGE_FIELD = "-6" 25 | ERR_MISSING_RECAPTCHA_RESPONSE_FIELD = "-7" 26 | ERR_MISSING_NEW = "-11" 27 | ERR_INCORRECT_PASSWORD = "-12" 28 | ERR_ACCOUNT_CREATED_VERIFICATION_SENT = "2" 29 | ERR_ACCOUNT_CREATED = "3" 30 | 31 | # for user.exists view 32 | USER_DOES_NOT_EXIST = "0" 33 | USER_EXIST = "1" 34 | -------------------------------------------------------------------------------- /weave/decorators.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | ''' 4 | Decorators for view functions inside weave. 5 | HTTP basic auth decorators taken from: 6 | http://www.djangosnippets.org/snippets/243/ 7 | 8 | Info: 9 | ~~~~~ 10 | to debug the output in a browser: add "?debug=1" to a url. 11 | To reformat the payload: add "?debug=2" to a url. 12 | (This works only if settings.DEBUG==True) 13 | 14 | Created on 15.03.2010 15 | 16 | @license: GNU GPL v3 or above, see LICENSE for more details. 17 | @copyright: 2010 see AUTHORS for more details. 18 | @author: Jens Diemer 19 | @author: Scanner (http://www.djangosnippets.org/users/Scanner/) 20 | @author: Michael Fladischer 21 | ''' 22 | 23 | from datetime import datetime 24 | import base64 25 | import pprint 26 | 27 | try: 28 | from functools import wraps 29 | except ImportError: 30 | from django.utils.functional import wraps # Python 2.3, 2.4 fallback. 31 | 32 | try: 33 | import json # New in Python v2.6 34 | except ImportError: 35 | from django.utils import simplejson as json 36 | 37 | from django.conf import settings 38 | from django.core.exceptions import PermissionDenied 39 | from django.http import HttpResponse, HttpResponseBadRequest, \ 40 | HttpResponseForbidden 41 | from django.contrib.auth import authenticate, login, logout 42 | 43 | from weave.utils import weave_timestamp, make_sync_hash 44 | from weave import Logging 45 | 46 | 47 | logger = Logging.get_logger() 48 | 49 | 50 | def view_or_basicauth(view, request, test_func, realm="", *args, **kwargs): 51 | """ 52 | This is a helper function used by both 'logged_in_or_basicauth' and 53 | 'has_perm_or_basicauth' that does the nitty of determining if they 54 | are already logged in or if they have provided proper http-authorization 55 | and returning the view if all goes well, otherwise responding with a 401. 56 | """ 57 | if test_func(request.user): 58 | # Already logged in, just return the view. 59 | return view(request, *args, **kwargs) 60 | 61 | if settings.WEAVE.DISABLE_LOGIN: 62 | # disable weave own basicauth login, user must login before 63 | # using firefox-sync e.g. use the django admin login page. 64 | msg = "Request forbidden, basicauth was disabled." 65 | logger.debug(msg) 66 | if settings.DEBUG: 67 | return HttpResponseForbidden(msg) 68 | else: 69 | return HttpResponseForbidden() 70 | 71 | # They are not logged in. See if they provided login credentials 72 | if 'HTTP_AUTHORIZATION' in request.META: 73 | logger.debug("HTTP_AUTHORIZATION: %r" % request.META['HTTP_AUTHORIZATION']) 74 | # NOTE: We are only support basic authentication for now. 75 | auth = request.META['HTTP_AUTHORIZATION'].split() 76 | if len(auth) != 2: 77 | logger.debug("HTTP_AUTHORIZATION wrong length.") 78 | return HttpResponseBadRequest() 79 | 80 | auth_type, auth_data = auth 81 | 82 | if auth_type.lower() != "basic": 83 | logger.debug("HTTP_AUTHORIZATION is not 'basic'") 84 | return HttpResponseBadRequest() 85 | 86 | username, password = base64.b64decode(auth_data).split(':') 87 | 88 | # username = _fix_username(username) 89 | # if len(username) > 30: 90 | # logger.error("Username %r is longer than 30 characters!" % username) 91 | # return HttpResponseBadRequest() 92 | 93 | if len(password) > 256: 94 | logger.error("Password %r is longer than 256 characters!" % password) 95 | return HttpResponseBadRequest() 96 | 97 | user = authenticate(username=username, password=password) 98 | if user is None: 99 | logger.debug("basicauth error: user %r unknown or password wrong." % username) 100 | else: 101 | if not user.is_active: 102 | logger.debug("basicauth error: user %r is not active." % username) 103 | else: 104 | login(request, user) 105 | request.user = user 106 | logger.debug("basicauth success: user %r logged in." % username) 107 | return view(request, *args, **kwargs) 108 | 109 | # Either they did not provide an authorization header or 110 | # something in the authorization attempt failed. Send a 401 111 | # back to them to ask them to authenticate. 112 | logger.debug("No HTTP_AUTHORIZATION send, yet.") 113 | response = HttpResponse() 114 | response.status_code = 401 # Unauthorized: request requires user authentication 115 | response['WWW-Authenticate'] = 'Basic realm="%s"' % realm 116 | return response 117 | 118 | 119 | def logged_in_or_basicauth(func, realm=settings.WEAVE.BASICAUTH_REALM): 120 | """ 121 | A simple decorator that requires a user to be logged in. If they are not 122 | logged in the request is examined for a 'authorization' header. 123 | 124 | If the header is present it is tested for basic authentication and 125 | the user is logged in with the provided credentials. 126 | 127 | If the header is not present a http 401 is sent back to the 128 | requestor to provide credentials. 129 | 130 | The purpose of this is that in several django projects I have needed 131 | several specific views that need to support basic authentication, yet the 132 | web site as a whole used django's provided authentication. 133 | 134 | The uses for this are for urls that are access programmatically such as 135 | by rss feed readers, yet the view requires a user to be logged in. Many rss 136 | readers support supplying the authentication credentials via http basic 137 | auth (and they do NOT support a redirect to a form where they post a 138 | username/password.) 139 | 140 | Use is simple: 141 | 142 | @logged_in_or_basicauth 143 | def your_view: 144 | ... 145 | 146 | You can provide the name of the realm to ask for authentication within. 147 | """ 148 | @wraps(func) 149 | def wrapper(request, *args, **kwargs): 150 | return view_or_basicauth(func, request, 151 | lambda u: u.is_authenticated(), 152 | realm, *args, **kwargs) 153 | return wrapper 154 | 155 | 156 | def has_perm_or_basicauth(func, perm, realm=""): 157 | """ 158 | This is similar to the above decorator 'logged_in_or_basicauth' 159 | except that it requires the logged in user to have a specific 160 | permission. 161 | 162 | Use: 163 | 164 | @logged_in_or_basicauth('asforums.view_forumcollection') 165 | def your_view: 166 | ... 167 | 168 | """ 169 | @wraps(func) 170 | def wrapper(request, *args, **kwargs): 171 | return view_or_basicauth(func, request, 172 | lambda u: u.has_perm(perm), 173 | realm, *args, **kwargs) 174 | return wrapper 175 | 176 | 177 | def weave_assert_username(func, key='username'): 178 | """ 179 | Decorator to check if the username from the URL is the one logged in. 180 | It uses the kwargs "username" as the default key but it can be changed by passing 181 | key='field' to the decorator. 182 | 183 | Use: 184 | 185 | @weave_assert_username 186 | def your_view(request, username): 187 | 188 | You can provide the key of the username field which is 'username' by default. 189 | """ 190 | @wraps(func) 191 | def wrapper(request, *args, **kwargs): 192 | # Test if username argument matches logged in user. 193 | # Weave uses lowercase usernames inside the URL!!! 194 | 195 | url_username = kwargs[key].lower() 196 | logger.debug("Raw userdata from url: %r" % url_username) 197 | 198 | if request.user.username.lower() == url_username: 199 | # XXX obsolete weave 1.0 API ? 200 | logger.debug("Plaintext username %r from url is ok." % url_username) 201 | return func(request, *args, **kwargs) 202 | 203 | if not len(url_username) == 32: 204 | msg = "Wrong length of url userdata: %i" % len(url_username) 205 | logger.debug(msg + "(should be 32 characters long)") 206 | raise PermissionDenied(msg) 207 | 208 | # check new API 209 | email = request.user.email 210 | sync_hash = make_sync_hash(email) 211 | if url_username.startswith(sync_hash): 212 | logger.debug("Email hash %r from url is ok." % url_username) 213 | return func(request, *args, **kwargs) 214 | 215 | logger.debug("Url userdata %r doesn't fit to user %s" % (url_username, request.user.username)) 216 | 217 | logger.info("Logout user %s" % request.user) 218 | logout(request) 219 | 220 | raise PermissionDenied("URL userdata doesn't fit to user from HTTP authentication.") 221 | 222 | return wrapper 223 | 224 | 225 | def weave_assert_version(version): 226 | """ 227 | Check the weave api version (comes from the url). 228 | 229 | Use: 230 | 231 | @weave_assert_version 232 | def your_view(request, username): 233 | """ 234 | def decorator(func): 235 | @wraps(func) 236 | def wrapper(request, *args, **kwargs): 237 | if not 'version' in kwargs: 238 | msg = "no version specified in URL" 239 | logger.error(msg) 240 | raise AssertionError(msg) 241 | 242 | url_version = kwargs['version'] 243 | if isinstance(version, (list, tuple)) and url_version in version: 244 | return func(request, *args, **kwargs) 245 | elif url_version == version: 246 | return func(request, *args, **kwargs) 247 | 248 | msg = "unsupported weave client version: %r" % url_version 249 | logger.error(msg) 250 | raise AssertionError(msg) 251 | 252 | return wrapper 253 | return decorator 254 | 255 | 256 | def weave_render_response(func): 257 | """ 258 | Decorator that checks for the presence of weave specific HTTP headers 259 | and formats output accordingly. 260 | 261 | Use: 262 | 263 | @weave_render_response 264 | def your_view(request): 265 | """ 266 | @wraps(func) 267 | def wrapper(request, *args, **kwargs): 268 | timedata = datetime.now() 269 | data = func(request, timestamp=timedata, *args, **kwargs) 270 | logger.debug("Raw response data for %r: %r" % (func.__name__, data)) 271 | response = HttpResponse() 272 | response["X-Weave-Timestamp"] = weave_timestamp(timedata) 273 | 274 | if settings.DEBUG and "debug" in request.GET: 275 | logger.debug("debug output for %r:" % func.__name__) 276 | 277 | if int(request.GET["debug"]) > 1: 278 | def load_payload(item): 279 | if "payload" in item: 280 | raw_payload = item["payload"] 281 | payload_dict = json.loads(raw_payload) 282 | item["payload"] = payload_dict 283 | return item 284 | 285 | if isinstance(data, list): 286 | data = [load_payload(item) for item in data] 287 | else: 288 | data = load_payload(data) 289 | 290 | response["content-type"] = "text/plain" 291 | response.content = json.dumps(data, indent=4) 292 | else: 293 | if request.META.get("HTTP_ACCEPT") == 'application/newlines' and isinstance(data, list): 294 | response.content = '\n'.join([json.dumps(element) for element in data]) + '\n' 295 | response["content-type"] = 'application/newlines' 296 | response['X-Weave-Records'] = len(data) 297 | else: 298 | response["content-type"] = "application/json" 299 | response.content = json.dumps(data) 300 | 301 | return response 302 | return wrapper 303 | 304 | 305 | def debug_sync_request(func): 306 | @wraps(func) 307 | def wrapper(request, *args, **kwargs): 308 | if not settings.WEAVE.DEBUG_REQUEST: 309 | return func(request, *args, **kwargs) 310 | 311 | logger.debug("view args: %s" % repr(args)) 312 | logger.debug("view kwargs: %s" % repr(kwargs)) 313 | 314 | response = func(request, *args, **kwargs) 315 | 316 | logger.debug("request.GET: %s" % repr(request.GET)) 317 | logger.debug("request.POST: %s" % pprint.pformat(request.POST)) 318 | logger.debug("response.status_code: %r" % response.status_code) 319 | logger.debug("response headers: %s" % repr(response.items())) 320 | logger.debug("response raw content: %s" % repr(response.content)) 321 | 322 | if response["content-type"] == "application/json": 323 | data = json.loads(response.content) 324 | logger.debug("content: %s" % json.dumps(data, indent=4)) 325 | 326 | return response 327 | return wrapper 328 | 329 | 330 | -------------------------------------------------------------------------------- /weave/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding model 'Collection' 12 | db.create_table('weave_collection', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 15 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 16 | ('name', self.gf('django.db.models.fields.CharField')(max_length=96)), 17 | ('site', self.gf('django.db.models.fields.related.ForeignKey')(default=1, to=orm['sites.Site'])), 18 | )) 19 | db.send_create_signal('weave', ['Collection']) 20 | 21 | # Adding model 'Wbo' 22 | db.create_table('weave_wbo', ( 23 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 24 | ('modified', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), 25 | ('collection', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['weave.Collection'], null=True, blank=True)), 26 | ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), 27 | ('wboid', self.gf('django.db.models.fields.CharField')(max_length=64)), 28 | ('parentid', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)), 29 | ('predecessorid', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)), 30 | ('sortindex', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 31 | ('payload', self.gf('django.db.models.fields.TextField')(blank=True)), 32 | )) 33 | db.send_create_signal('weave', ['Wbo']) 34 | 35 | 36 | def backwards(self, orm): 37 | 38 | # Deleting model 'Collection' 39 | db.delete_table('weave_collection') 40 | 41 | # Deleting model 'Wbo' 42 | db.delete_table('weave_wbo') 43 | 44 | 45 | models = { 46 | 'auth.group': { 47 | 'Meta': {'object_name': 'Group'}, 48 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 49 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 50 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 51 | }, 52 | 'auth.permission': { 53 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 54 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 55 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 56 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 57 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 58 | }, 59 | 'auth.user': { 60 | 'Meta': {'object_name': 'User'}, 61 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 62 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 63 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 64 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 65 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 66 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 67 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 68 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 69 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 70 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 71 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 72 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 73 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 74 | }, 75 | 'contenttypes.contenttype': { 76 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 77 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 78 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 79 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 80 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 81 | }, 82 | 'sites.site': { 83 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 84 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 85 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 86 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 87 | }, 88 | 'weave.collection': { 89 | 'Meta': {'ordering': "('-modified',)", 'object_name': 'Collection'}, 90 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 91 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 92 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '96'}), 93 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['sites.Site']"}), 94 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 95 | }, 96 | 'weave.wbo': { 97 | 'Meta': {'ordering': "('-modified',)", 'object_name': 'Wbo'}, 98 | 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['weave.Collection']", 'null': 'True', 'blank': 'True'}), 99 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 100 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 101 | 'parentid': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), 102 | 'payload': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 103 | 'predecessorid': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), 104 | 'sortindex': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 105 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 106 | 'wboid': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 107 | } 108 | } 109 | 110 | complete_apps = ['weave'] 111 | -------------------------------------------------------------------------------- /weave/migrations/0002_add_field_wbo_ttl.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | class Migration(SchemaMigration): 8 | 9 | def forwards(self, orm): 10 | 11 | # Adding field 'Wbo.ttl' 12 | db.add_column('weave_wbo', 'ttl', models.IntegerField(null=True, blank=True), keep_default=False) 13 | 14 | 15 | def backwards(self, orm): 16 | 17 | # Deleting field 'Wbo.ttl' 18 | db.delete_column('weave_wbo', 'ttl') 19 | 20 | 21 | models = { 22 | 'auth.group': { 23 | 'Meta': {'object_name': 'Group'}, 24 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 25 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 26 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 27 | }, 28 | 'auth.permission': { 29 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 30 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 31 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 34 | }, 35 | 'auth.user': { 36 | 'Meta': {'object_name': 'User'}, 37 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 38 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 39 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 40 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 41 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 42 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 43 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 44 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 45 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 46 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 47 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 48 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 49 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 50 | }, 51 | 'contenttypes.contenttype': { 52 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 53 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 54 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 55 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 56 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 57 | }, 58 | 'sites.site': { 59 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 60 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 61 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 62 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 63 | }, 64 | 'weave.collection': { 65 | 'Meta': {'ordering': "('-modified',)", 'object_name': 'Collection'}, 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 68 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '96'}), 69 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['sites.Site']"}), 70 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 71 | }, 72 | 'weave.wbo': { 73 | 'Meta': {'ordering': "('-modified',)", 'object_name': 'Wbo'}, 74 | 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['weave.Collection']", 'null': 'True', 'blank': 'True'}), 75 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 76 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 77 | 'parentid': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), 78 | 'payload': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 79 | 'payload_size': ('django.db.models.fields.PositiveIntegerField', [], {}), 80 | 'predecessorid': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), 81 | 'sortindex': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 82 | 'ttl': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 83 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 84 | 'wboid': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 85 | } 86 | } 87 | 88 | complete_apps = ['weave'] 89 | -------------------------------------------------------------------------------- /weave/migrations/0003_add_field_payload_size.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | from south.db import db 4 | from south.v2 import DataMigration 5 | from django.db import models 6 | 7 | class Migration(DataMigration): 8 | 9 | def forwards(self, orm): 10 | # Adding field 'Wbo.payload_size' 11 | db.add_column('weave_wbo', 'payload_size', self.gf('django.db.models.fields.PositiveIntegerField')(default=0), keep_default=False) 12 | 13 | # Calculate payload size for existing entries 14 | for wbo in orm.Wbo.objects.all(): 15 | payload = wbo.payload 16 | payload_size = len(payload) 17 | wbo.payload_size = payload_size 18 | wbo.save() 19 | 20 | 21 | def backwards(self, orm): 22 | # Deleting field 'Wbo.payload_size' 23 | db.delete_column('weave_wbo', 'payload_size') 24 | 25 | 26 | models = { 27 | 'auth.group': { 28 | 'Meta': {'object_name': 'Group'}, 29 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 30 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), 31 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) 32 | }, 33 | 'auth.permission': { 34 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, 35 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 36 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), 37 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 38 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 39 | }, 40 | 'auth.user': { 41 | 'Meta': {'object_name': 'User'}, 42 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 43 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 44 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 45 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), 46 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 47 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), 48 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 49 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), 50 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 51 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), 52 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), 53 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), 54 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) 55 | }, 56 | 'contenttypes.contenttype': { 57 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 58 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 59 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 60 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 61 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 62 | }, 63 | 'sites.site': { 64 | 'Meta': {'ordering': "('domain',)", 'object_name': 'Site', 'db_table': "'django_site'"}, 65 | 'domain': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 66 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 67 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 68 | }, 69 | 'weave.collection': { 70 | 'Meta': {'ordering': "('-modified',)", 'object_name': 'Collection'}, 71 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 72 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 73 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '96'}), 74 | 'site': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['sites.Site']"}), 75 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) 76 | }, 77 | 'weave.wbo': { 78 | 'Meta': {'ordering': "('-modified',)", 'object_name': 'Wbo'}, 79 | 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['weave.Collection']", 'null': 'True', 'blank': 'True'}), 80 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 81 | 'modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), 82 | 'parentid': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), 83 | 'payload': ('django.db.models.fields.TextField', [], {'blank': 'True'}), 84 | 'payload_size': ('django.db.models.fields.PositiveIntegerField', [], {}), 85 | 'predecessorid': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), 86 | 'sortindex': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 87 | 'ttl': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 88 | 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), 89 | 'wboid': ('django.db.models.fields.CharField', [], {'max_length': '64'}) 90 | } 91 | } 92 | 93 | complete_apps = ['weave'] 94 | -------------------------------------------------------------------------------- /weave/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discontinue/django-sync-server/afe98dca07f08e36280143a2be9b8c4bad11fb81/weave/migrations/__init__.py -------------------------------------------------------------------------------- /weave/models.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Models. 3 | FIXME: I dropped site framework integration for the sake 4 | of simpler debugging. Add it back once API is stable 5 | and working. 6 | 7 | Created on 15.03.2010 8 | 9 | @license: GNU GPL v3 or above, see LICENSE for more details. 10 | @copyright: 2010-2013 see AUTHORS for more details. 11 | @author: Jens Diemer 12 | @author: FladischerMichael 13 | ''' 14 | 15 | from django.conf import settings 16 | from django.db import models 17 | from django.core.exceptions import ValidationError 18 | from django.contrib.auth.models import User 19 | from django.contrib.sites.models import Site 20 | from django.contrib.sites.managers import CurrentSiteManager 21 | 22 | from weave.utils import weave_timestamp 23 | from weave import Logging 24 | 25 | logger = Logging.get_logger() 26 | 27 | 28 | class BaseModel(models.Model): 29 | modified = models.DateTimeField(auto_now=True, help_text="Time of the last change.") 30 | 31 | class Meta: 32 | abstract = True 33 | 34 | 35 | class CollectionManager(CurrentSiteManager): 36 | def create_or_update(self, user, col_name, timestamp, since=None): 37 | logger.debug("Created or update collection %s for user %s" % (col_name, user.username)) 38 | 39 | collection, created = super(CollectionManager, self).get_or_create( 40 | user=user, name=col_name, 41 | ) 42 | 43 | # See if we have a constraint on the last modified date 44 | if since is not None: 45 | if since < collection.modified: 46 | raise ValidationError 47 | 48 | collection.modified = timestamp 49 | collection.save() 50 | 51 | if created: 52 | logger.debug("New collection %s created." % collection) 53 | else: 54 | logger.debug("Existing collection %s updated." % collection) 55 | return collection, created 56 | 57 | 58 | class Collection(BaseModel): 59 | """ 60 | http://docs.services.mozilla.com/storage/apis-1.1.html 61 | 62 | inherited from BaseModel: 63 | modified -> datetime of the last change 64 | """ 65 | user = models.ForeignKey(User) 66 | name = models.CharField(max_length=96) 67 | 68 | site = models.ForeignKey(Site, editable=False, default=settings.SITE_ID) 69 | on_site = CollectionManager('site') 70 | 71 | def __unicode__(self): 72 | return u"%r (user: %r, site: %r)" % (self.name, self.user.username, self.site) 73 | 74 | class Meta: 75 | ordering = ("-modified",) 76 | 77 | 78 | class WboManager(models.Manager): 79 | def create_or_update(self, payload_dict, collection, user, timestamp): 80 | """ 81 | create or update a wbo 82 | TODO: 83 | - Check parentid, but how? 84 | - must wboid + parentid be unique? 85 | """ 86 | logger.debug("Created or update WBO for collection %s" % collection) 87 | 88 | payload = payload_dict['payload'] 89 | payload_size = len(payload) 90 | 91 | wbo, created = Wbo.objects.get_or_create( 92 | collection=collection, 93 | user=user, 94 | wboid=payload_dict['id'], 95 | defaults={ 96 | 'parentid': payload_dict.get('parentid', None), 97 | 'predecessorid': payload_dict.get('predecessorid', None), 98 | 'sortindex': payload_dict.get('sortindex', None), 99 | 'ttl': payload_dict.get('ttl', None), 100 | 'modified': timestamp, 101 | 'payload_size': payload_size, 102 | 'payload': payload, 103 | } 104 | ) 105 | if created: 106 | logger.debug("New wbo created: %r" % wbo) 107 | else: 108 | wbo.parentid = payload_dict.get("parentid", None) 109 | wbo.predecessorid = payload_dict.get("predecessorid", None) 110 | wbo.sortindex = payload_dict.get("sortindex", None) 111 | wbo.ttl = payload_dict.get("ttl", None) 112 | wbo.modified = timestamp 113 | wbo.payload_size = payload_size 114 | wbo.payload = payload 115 | wbo.save() 116 | logger.debug("Existing wbo updated: %r" % wbo) 117 | 118 | return wbo, created 119 | 120 | 121 | class Wbo(BaseModel): 122 | """ 123 | http://docs.services.mozilla.com/storage/apis-1.1.html 124 | 125 | inherited from BaseModel: 126 | modified -> datetime of the last change 127 | """ 128 | objects = WboManager() 129 | collection = models.ForeignKey(Collection, blank=True, null=True) 130 | user = models.ForeignKey(User) 131 | wboid = models.CharField(max_length=64, 132 | help_text="wbo identifying string" 133 | ) 134 | parentid = models.CharField(max_length=64, blank=True, null=True, 135 | help_text="wbo parent identifying string" 136 | ) 137 | predecessorid = models.CharField(max_length=64, blank=True, null=True, 138 | help_text="wbo predecessorid" 139 | ) 140 | sortindex = models.IntegerField(blank=True, null=True, 141 | help_text="An integer indicting the relative importance of this item in the collection." 142 | ) 143 | payload = models.TextField(blank=True, 144 | help_text=( 145 | "A string containing a JSON structure encapsulating the data of the record." 146 | " This structure is defined separately for each WBO type. Parts of the" 147 | " structure may be encrypted, in which case the structure should also" 148 | " specify a record for decryption." 149 | ) 150 | ) 151 | payload_size = models.PositiveIntegerField(help_text="Size of the payload.") 152 | ttl = models.IntegerField(blank=True, null=True, 153 | help_text=( 154 | "The number of seconds to keep this record." 155 | " After that time, this item will not be returned." 156 | ) 157 | ) 158 | 159 | def clean(self): 160 | # Don't allow draft entries to have a pub_date. 161 | if self.ttl is not None: 162 | if self.ttl < 0 or self.ttl > 31536000: 163 | # from https://hg.mozilla.org/services/server-storage/file/830a414aaed7/syncstorage/wbo.py#l80 164 | raise ValidationError('TTL %r out of range.' % self.ttl) 165 | 166 | def save(self, *args, **kwarg): 167 | self.full_clean() 168 | super(Wbo, self).save(*args, **kwarg) 169 | 170 | def get_response_dict(self): 171 | response_dict = { 172 | "id": self.wboid, 173 | "modified": weave_timestamp(self.modified), 174 | "payload": self.payload, 175 | } 176 | # Don't send additional properties if payload is emtpy -> deleted Wbo 177 | if self.payload != '': 178 | for key in ("parentid", "predecessorid", "sortindex"): 179 | value = getattr(self, key) 180 | if value: 181 | response_dict[key] = value 182 | return response_dict 183 | 184 | def __unicode__(self): 185 | return u"%r (%r)" % (self.wboid, self.collection) 186 | 187 | class Meta: 188 | ordering = ("-modified",) 189 | -------------------------------------------------------------------------------- /weave/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block title %}django-sync-server - Page not found (404){% endblock %} 4 | 5 | {% block content %} 6 |

Page not found (404)

7 |

We're sorry, but the requested object could not be found.

8 | {% endblock %} -------------------------------------------------------------------------------- /weave/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block title %}django-sync-server - Server error (500){% endblock %} 4 | 5 | {% block content %} 6 |

Server Error (500)

7 |

There's been an error. It's been reported to the site administrators via e-mail and should be fixed shortly. Thanks for your patience.

8 |

You see not the debug error page, because debugging is off or your IP is not in settings.INTERNAL_IPS

9 | {% endblock %} -------------------------------------------------------------------------------- /weave/templates/weave/info_page.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | 3 | {% block branding %} 4 |

{{ title }}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |

9 | Use {{ server_url }} as server url in your weave client preferences. 10 |

11 | 12 | {% if request.user.is_authenticated %} 13 |

You logged in as {{ request.user }}.

14 |

User statistics

15 |
16 |
WBO count
17 |
{{ wbo_count }}
18 |
Payload size together
19 |
{{ payload_size|filesizeformat }}
20 |
Latest entry
21 |
{{ latest_modified|timesince }}
22 |
Oldest entry
23 |
{{ oldest_modified|timesince }}
24 |
25 |

(Calculated in {{ duration|stringformat:".2f" }}sec. )

26 | {% else %} 27 |

You not logged in. No statistics available for anonymous user.

28 | {% endif %} 29 |

30 | How to enable sync debug, visit DebugHelp wiki page. 31 |

32 | {% endblock content %} 33 | 34 | {% block footer %} 35 | 38 | {% endblock %} -------------------------------------------------------------------------------- /weave/tests.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | 3 | """ 4 | django-sync-server unittests 5 | ~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | :copyleft: 2010 by the django-sync-server team, see AUTHORS for more details. 8 | :license: GNU GPL v3 or above, see LICENSE for more details. 9 | """ 10 | 11 | import base64 12 | import logging 13 | import time 14 | 15 | 16 | if __name__ == "__main__": 17 | # run unittest directly 18 | # this works only in a created test virtualenv, see: 19 | # http://code.google.com/p/django-sync-server/wiki/CreateTestEnvironment 20 | import os 21 | os.environ["DJANGO_SETTINGS_MODULE"] = "testproject.settings" 22 | 23 | 24 | from django.core.exceptions import ValidationError 25 | from django.contrib.auth.models import User 26 | from django.core.urlresolvers import reverse 27 | from django.test.client import Client 28 | from django.test import TestCase 29 | from django.conf import settings 30 | 31 | #from django_tools.utils import info_print 32 | #info_print.redirect_stdout() 33 | 34 | from weave.models import Wbo, Collection 35 | from weave.views import sync 36 | from weave import Logging 37 | from weave.utils import make_sync_hash 38 | 39 | 40 | def _enable_logging(): 41 | logger = Logging.get_logger() 42 | handler = logging.StreamHandler() 43 | logger.addHandler(handler) 44 | 45 | 46 | class WeaveServerTest(TestCase): 47 | def setUp(self, *args, **kwargs): 48 | super(WeaveServerTest, self).setUp(*args, **kwargs) 49 | 50 | settings.WEAVE.DISABLE_LOGIN = False 51 | 52 | # Create a test user with basic auth data 53 | self.testuser = User(username="testuser") 54 | raw_password = "test user password!" 55 | self.testuser.set_password(raw_password) 56 | self.testuser.save() 57 | 58 | raw_auth_data = "%s:%s" % (self.testuser.username, raw_password) 59 | self.auth_data = "basic %s" % base64.b64encode(raw_auth_data) 60 | 61 | self.client = Client() 62 | 63 | def tearDown(self, *args, **kwargs): 64 | super(WeaveServerTest, self).tearDown(*args, **kwargs) 65 | self.testuser.delete() 66 | 67 | def assertWeaveTimestamp(self, response): 68 | """ Check if a valid weave timestamp is in response. """ 69 | key = "x-weave-timestamp" 70 | try: 71 | raw_timestamp = response[key] 72 | except KeyError, err: 73 | self.fail("Weave timestamp (%s) not in response." % key) 74 | 75 | timestamp = float(raw_timestamp) 76 | comparison_value = time.time() - 1 77 | self.failUnless(timestamp > comparison_value, 78 | "Weave timestamp %r is not bigger than comparison value %r" % (timestamp, comparison_value) 79 | ) 80 | 81 | def test_register_check_user_not_exist(self): 82 | """ test user.register_check view with not existing user. """ 83 | url = reverse("weave-register_check", kwargs={"username":"user doesn't exist"}) 84 | response = self.client.get(url) 85 | self.failUnlessEqual(response.content, "1") 86 | 87 | def test_register_check_user_exist(self): 88 | """ test user.register_check view with existing test user. """ 89 | url = reverse("weave-register_check", kwargs={"username":self.testuser.username}) 90 | response = self.client.get(url) 91 | self.failUnlessEqual(response.content, "0") 92 | 93 | def test_exists_with_not_existing_user(self): 94 | """ test user.exists view with not existing user. """ 95 | url = reverse("weave-exists", kwargs={"username":"user doesn't exist", "version":"1.0"}) 96 | response = self.client.get(url) 97 | self.failUnlessEqual(response.content, "0") 98 | 99 | def test_exists_with_existing_user(self): 100 | """ test user.exists view with existing test user. """ 101 | url = reverse("weave-exists", kwargs={"username":self.testuser.username, "version":"1.0"}) 102 | response = self.client.get(url) 103 | self.failUnlessEqual(response.content, "1") 104 | 105 | def test_basicauth_get_authenticate(self): 106 | """ test if we get 401 'unauthorized' response. """ 107 | url = reverse("weave-info", kwargs={"username":self.testuser.username, "version":"1.0"}) 108 | response = self.client.get(url) 109 | self.failUnlessEqual(response.status_code, 401) # Unauthorized: request requires user authentication 110 | self.failUnlessEqual( 111 | response["www-authenticate"], 'Basic realm="%s"' % settings.WEAVE.BASICAUTH_REALM 112 | ) 113 | self.failUnlessEqual(response.content, "") 114 | 115 | def test_disable_basicauth(self): 116 | """ We should not get a basicauth response, if login is disabled. """ 117 | settings.WEAVE.DISABLE_LOGIN = True 118 | url = reverse("weave-info", kwargs={"username":self.testuser.username, "version":"1.0"}) 119 | response = self.client.get(url) 120 | self.failUnlessEqual(response.status_code, 403) # Forbidden 121 | self.failIf("www-authenticate" in response) 122 | 123 | def test_basicauth_send_authenticate(self): 124 | """ test if we can login via basicauth. """ 125 | url = reverse("weave-info", kwargs={"username":self.testuser.username, "version":"1.1"}) 126 | response = self.client.get(url, HTTP_AUTHORIZATION=self.auth_data) 127 | self.failUnlessEqual(response.status_code, 200) 128 | self.failUnlessEqual(response.content, "{}") 129 | self.failUnlessEqual(response["content-type"], "application/json") 130 | self.assertWeaveTimestamp(response) 131 | 132 | def test_create_wbo(self): 133 | url = reverse("weave-col_storage", kwargs={"username":"testuser", "version":"1.1", "col_name":"foobar"}) 134 | data = ( 135 | u'[{"id": "12345678-90AB-CDEF-1234-567890ABCDEF", "payload": "This is the payload"}]' 136 | ) 137 | response = self.client.post(url, data=data, content_type="application/json", HTTP_AUTHORIZATION=self.auth_data) 138 | self.failUnlessEqual(response.content, u'{"failed": [], "success": ["12345678-90AB-CDEF-1234-567890ABCDEF"]}') 139 | self.failUnlessEqual(response["content-type"], "application/json") 140 | 141 | def test_wbo_ttl_out_of_range1(self): 142 | self.assertRaises( 143 | ValidationError, 144 | Wbo.objects.create, 145 | user=self.testuser, wboid="1", payload="", payload_size=0, ttl= -1, 146 | ) 147 | def test_wbo_ttl_out_of_range2(self): 148 | self.assertRaises( 149 | ValidationError, 150 | Wbo.objects.create, 151 | user=self.testuser, wboid="1", payload="", payload_size=0, ttl=31536001, 152 | ) 153 | 154 | def test_post_wbo_ttl_out_of_range1(self): 155 | url = reverse(sync.storage, kwargs={"username":"testuser", "version":"1.1", "col_name":"foobar"}) 156 | data = ( 157 | u'[{"id": "1", "payload": "This is the payload", "ttl": -1}]' 158 | ) 159 | response = self.client.post(url, data=data, content_type="application/json", HTTP_AUTHORIZATION=self.auth_data) 160 | self.failUnlessEqual(response.content, u'{"failed": ["1"], "success": []}') 161 | self.failUnlessEqual(response["content-type"], "application/json") 162 | 163 | def test_post_wbo_ttl_out_of_range2(self): 164 | url = reverse(sync.storage, kwargs={"username":"testuser", "version":"1.1", "col_name":"foobar"}) 165 | # settings.DEBUG = True 166 | # url += "?debug=1" 167 | data = ( 168 | u'[{"id": "1", "payload": "This is the payload", "ttl": 31536001}]' 169 | ) 170 | response = self.client.post(url, data=data, content_type="application/json", HTTP_AUTHORIZATION=self.auth_data) 171 | self.failUnlessEqual(response.content, u'{"failed": ["1"], "success": []}') 172 | self.failUnlessEqual(response["content-type"], "application/json") 173 | 174 | def test_csrf_exempt(self): 175 | url = reverse("weave-col_storage", kwargs={"username":"testuser", "version":"1.1", "col_name":"foobar"}) 176 | data = ( 177 | u'[{"id": "12345678-90AB-CDEF-1234-567890ABCDEF", "payload": "This is the payload"}]' 178 | ) 179 | csrf_client = Client(enforce_csrf_checks=True) 180 | 181 | response = csrf_client.post(url, data=data, content_type="application/json", HTTP_AUTHORIZATION=self.auth_data) 182 | 183 | self.failUnlessEqual(response.content, u'{"failed": [], "success": ["12345678-90AB-CDEF-1234-567890ABCDEF"]}') 184 | self.failUnlessEqual(response["content-type"], "application/json") 185 | 186 | # Check if the csrf_exempt adds the csrf_exempt attribute to response: 187 | self.failUnlessEqual(response.csrf_exempt, True) 188 | 189 | def test_delete_not_existing_wbo(self): 190 | """ 191 | http://github.com/jedie/django-sync-server/issues#issue/6 192 | """ 193 | url = reverse("weave-wbo_storage", 194 | kwargs={ 195 | "username":self.testuser.username, "version":"1.1", 196 | "col_name": "foobar", "wboid": "doesn't exist", 197 | } 198 | ) 199 | response = self.client.delete(url, HTTP_AUTHORIZATION=self.auth_data) 200 | self.assertContains(response, "Page not found (404)", count=None, status_code=404) 201 | self.failUnlessEqual(response["content-type"], "text/html; charset=utf-8") 202 | 203 | # from django_tools.unittest_utils.BrowserDebug import debug_response 204 | # debug_response(response) 205 | 206 | class WeaveServerUserTest(TestCase): 207 | def test_create_user(self): 208 | # _enable_logging() 209 | email = u"test@test.tld" 210 | sync_hash = make_sync_hash(email) 211 | url = reverse("weave-exists", kwargs={"username":sync_hash, "version":"1.0"}) 212 | 213 | data = u'{"password": "12345678", "email": "%s", "captcha-challenge": null, "captcha-response": null}' % email 214 | 215 | # Bug in django? The post data doesn't transfered to view, if self.client.put() used 216 | # response = self.client.put(url, data=data, content_type="application/json") 217 | 218 | # But this works: 219 | response = self.client.post(url, data=data, content_type="application/json", 220 | REQUEST_METHOD="PUT" 221 | ) 222 | 223 | self.failUnlessEqual(response.content, u"") 224 | 225 | # Check if user was created: 226 | user = User.objects.get(username=sync_hash) 227 | self.failUnlessEqual(user.email, email) 228 | 229 | 230 | #__test__ = {"doctest": """ 231 | #Another way to test that 1 + 1 is equal to 2. 232 | # 233 | #>>> 1 + 1 == 2 234 | #True 235 | #"""} 236 | 237 | if __name__ == "__main__": 238 | # Run all unittest directly 239 | from django.core import management 240 | 241 | tests = "weave" 242 | # tests = "weave.WeaveServerTest.test_create_user" 243 | # tests = "weave.WeaveServerTest.test_csrf_exempt" 244 | # tests = "weave.WeaveServerTest.test_post_wbo_ttl_out_of_range2" 245 | 246 | management.call_command('test', tests, verbosity=1) 247 | -------------------------------------------------------------------------------- /weave/urls.py: -------------------------------------------------------------------------------- 1 | ''' 2 | URL mapping. 3 | 4 | Created on 15.03.2010 5 | 6 | @license: GNU GPL v3 or above, see LICENSE for more details. 7 | @copyright: 2010 see AUTHORS for more details. 8 | @author: Jens Diemer 9 | @author: FladischerMichael 10 | ''' 11 | 12 | from django.conf.urls.defaults import patterns, url 13 | 14 | from weave.views import sync, user, misc 15 | 16 | urlpatterns = patterns('weave', 17 | url(r'^(?P[\d\.]+?)/(?P.*?)/storage/(?P.*?)/(?P.*?)$', sync.storage, name="weave-wbo_storage"), 18 | url(r'^(?P[\d\.]+?)/(?P.*?)/storage/(?P.*?)$', sync.storage, name="weave-col_storage"), 19 | url(r'^(?P[\d\.]+?)/(?P.*?)/storage$', sync.storage), 20 | url(r'^(?P[\d\.]+?)/(?P.*?)/info/collections[/]?$', sync.info, name="weave-info"), 21 | url(r'^misc/(?P[\d\.]+?)/captcha_html$', misc.captcha, name="weave-captcha"), 22 | url(r'^user/(?P[\d\.]+?)/(?P.*?)/node/weave$', user.node, name="weave-node"), 23 | url(r'^user/(?P[\d\.]+?)/(?P.*?)/password$', user.password, name="weave-password"), 24 | url(r'^user/(?P[\d\.]+?)/(?P.*?)$', user.exists, name="weave-exists"), 25 | url(r'^api/register/check/(?P.*?)$', user.register_check, name="weave-register_check"), 26 | url(r'^weave-password-reset$', user.password_reset, name="weave-password_reset"), 27 | url(r'^$', misc.info_page, name="weave-info_page"), 28 | ) 29 | -------------------------------------------------------------------------------- /weave/utils.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | ''' 4 | Utility functions for Weave API. 5 | 6 | Created on 15.03.2010 7 | 8 | @license: GNU GPL v3 or above, see LICENSE for more details. 9 | @copyright: 2010-2011 see AUTHORS for more details. 10 | @author: Jens Diemer 11 | @author: FladischerMichael 12 | ''' 13 | 14 | from datetime import datetime 15 | import base64 16 | import hashlib 17 | import time 18 | 19 | from weave import Logging 20 | 21 | logger = Logging.get_logger() 22 | 23 | 24 | def weave_timestamp(timedata=None): 25 | if timedata is None: 26 | timedata = datetime.now() 27 | return time.mktime(timedata.timetuple()) 28 | 29 | 30 | def limit_wbo_queryset(request, queryset): 31 | """ 32 | TODO: 33 | predecessorid = fromform(form, "predecessorid") 34 | full = fromform(form, "full") 35 | """ 36 | GET = request.GET 37 | 38 | ids = GET.get("ids", None) 39 | if ids is not None: 40 | ids = ids.split(",") 41 | queryset = queryset.filter(wboid__in=ids) 42 | 43 | parentid = GET.get("parentid", None) 44 | if parentid is not None: 45 | queryset = queryset.filter(parentid=parentid) 46 | 47 | newer = GET.get("newer", None) 48 | if newer is not None: # Greater than or equal to newer modified timestamp 49 | queryset = queryset.filter(modified__gte=datetime.fromtimestamp(float(newer))) 50 | 51 | older = GET.get("older", None) 52 | if older is not None: # Less than or equal to older modified timestamp 53 | queryset = queryset.filter(modified__lte=datetime.fromtimestamp(float(older))) 54 | 55 | index_above = GET.get("index_above", None) 56 | if index_above is not None: # Greater than or equal to index_above modified timestamp 57 | queryset = queryset.filter(sortindex__gte=int(index_above)) 58 | 59 | index_below = GET.get("index_below", None) 60 | if index_below is not None: # Less than or equal to index_below modified timestamp 61 | queryset = queryset.filter(sortindex__lte=int(index_below)) 62 | 63 | sort_type = GET.get("sort", None) 64 | if sort_type is not None: 65 | if sort_type == 'oldest': 66 | queryset = queryset.order_by("modified") 67 | elif sort_type == 'newest': 68 | queryset = queryset.order_by("-modified") 69 | elif sort_type == 'index': 70 | queryset = queryset.order_by("wboid") 71 | else: 72 | raise NameError("sort type %r unknown" % sort_type) 73 | 74 | offset = GET.get("offset", None) 75 | if offset is not None: 76 | queryset = queryset[int(offset):] 77 | 78 | limit = GET.get("limit", None) 79 | if limit is not None: 80 | queryset = queryset[:int(limit)] 81 | 82 | return queryset 83 | 84 | 85 | def make_sync_hash(txt): 86 | """ 87 | make a base32 encoded SHA1 hash value. 88 | Used in firefox sync for creating a username from the email address. 89 | See also: 90 | https://hg.mozilla.org/services/minimal-server/file/5ee9d9a4570a/weave_minimal/create_user#l87 91 | """ 92 | sha1 = hashlib.sha1(txt).digest() 93 | base32encode = base64.b32encode(sha1).lower() 94 | return base32encode 95 | -------------------------------------------------------------------------------- /weave/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/discontinue/django-sync-server/afe98dca07f08e36280143a2be9b8c4bad11fb81/weave/views/__init__.py -------------------------------------------------------------------------------- /weave/views/misc.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | ''' 4 | misc views 5 | ~~~~~~~~~~ 6 | 7 | Created on 27.03.2010 8 | 9 | Due to Mozilla Weave supporting Recaptcha solely, we have to stick with it until 10 | they decide to change the interface to pluggable captchas. 11 | 12 | @license: GNU GPL v3 or above, see LICENSE for more details. 13 | @copyleft: 2010-2012 by the django-sync-server team, see AUTHORS for more details. 14 | ''' 15 | 16 | import time 17 | 18 | from django.conf import settings 19 | from django.core.exceptions import ImproperlyConfigured 20 | from django.http import HttpResponse 21 | from django.shortcuts import render_to_response 22 | from django.template import RequestContext 23 | from django.views.decorators.csrf import csrf_exempt 24 | 25 | # django-sync-server own stuff 26 | from weave import Logging, VERSION_STRING 27 | from weave.decorators import weave_assert_version, debug_sync_request 28 | from weave.models import Wbo 29 | 30 | 31 | logger = Logging.get_logger() 32 | 33 | 34 | @debug_sync_request 35 | @weave_assert_version(("1.0", "1.1")) 36 | @csrf_exempt 37 | def captcha(request, version): 38 | if settings.WEAVE.DONT_USE_CAPTCHA == True: 39 | logger.warn("You should activate captcha!")# 40 | return HttpResponse("Captcha support is disabled.") 41 | 42 | # Check for aviability of recaptcha 43 | # (can be found at: http://pypi.python.org/pypi/recaptcha-client) 44 | try: 45 | from recaptcha.client.captcha import displayhtml 46 | except ImportError: 47 | logger.error("Captcha requested but unable to import the 'recaptcha' package!") 48 | return HttpResponse("Captcha support disabled due to missing Python package 'recaptcha'.") 49 | if not getattr(settings.WEAVE, "RECAPTCHA_PUBLIC_KEY"): 50 | logger.error("Trying to create user but settings.WEAVE.RECAPTCHA_PUBLIC_KEY not set") 51 | raise ImproperlyConfigured 52 | # Send a simple HTML to the client. It get's rendered inside the Weave client. 53 | return HttpResponse("%s" % displayhtml(settings.WEAVE.RECAPTCHA_PUBLIC_KEY)) 54 | 55 | 56 | def info_page(request): 57 | server_url = request.build_absolute_uri(request.path) 58 | if not server_url.endswith("/"): 59 | # sync setup dialog only accept the server url if it's ends with a slash 60 | server_url += "/" 61 | 62 | context = { 63 | "title": "django-sync-server - info page", 64 | "request": request, 65 | "weave_version": VERSION_STRING, 66 | "server_url":server_url, 67 | } 68 | 69 | if request.user.is_authenticated() and request.user.is_active: 70 | start_time = time.time() 71 | 72 | payload_queryset = Wbo.objects.filter(user=request.user.id).only("payload") 73 | 74 | wbo_count = 0 75 | payload_size = 0 76 | for item in payload_queryset.iterator(): 77 | wbo_count += 1 78 | payload_size += len(item.payload) 79 | 80 | modified_queryset = Wbo.objects.filter(user=request.user.id).only("modified") 81 | try: 82 | latest = modified_queryset.latest("modified").modified 83 | except Wbo.DoesNotExist: 84 | # User hasn't used sync, so no WBOs exist from him 85 | latest = None 86 | oldest = None 87 | else: 88 | oldest = modified_queryset.order_by("modified")[0].modified 89 | 90 | duration = time.time() - start_time 91 | 92 | context.update({ 93 | "wbo_count": wbo_count, 94 | "payload_size": payload_size, 95 | "duration": duration, 96 | "latest_modified": latest, 97 | "oldest_modified": oldest, 98 | }) 99 | 100 | return render_to_response("weave/info_page.html", context, context_instance=RequestContext(request)) 101 | -------------------------------------------------------------------------------- /weave/views/sync.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | ''' 4 | Storage for Weave. 5 | 6 | Created on 15.03.2010 7 | 8 | @license: GNU GPL v3 or above, see LICENSE for more details. 9 | @copyleft: 2010-2013 by the django-sync-server team, see AUTHORS for more details. 10 | ''' 11 | 12 | from datetime import datetime 13 | 14 | try: 15 | import json # New in Python v2.6 16 | except ImportError: 17 | from django.utils import simplejson as json 18 | 19 | from django.conf import settings 20 | from django.views.decorators.csrf import csrf_exempt 21 | from django.core.exceptions import ValidationError 22 | from django.http import Http404 23 | from django.shortcuts import get_object_or_404 24 | 25 | # django-sync-server own stuff 26 | from weave.models import Collection, Wbo 27 | from weave.utils import limit_wbo_queryset, weave_timestamp 28 | from weave.decorators import weave_assert_username, weave_assert_version, \ 29 | logged_in_or_basicauth, weave_render_response, debug_sync_request 30 | from weave import Logging 31 | 32 | logger = Logging.get_logger() 33 | 34 | 35 | @debug_sync_request 36 | @logged_in_or_basicauth 37 | @weave_assert_version(("1.0", "1.1")) 38 | @weave_assert_username 39 | @csrf_exempt 40 | @weave_render_response 41 | def info(request, version, username, timestamp): 42 | """ 43 | return all collection keys with the timestamp 44 | https://wiki.mozilla.org/Labs/Weave/Sync/1.0/API#GET 45 | """ 46 | collections = {} 47 | for collection in Collection.on_site.filter(user=request.user): 48 | collections[collection.name] = weave_timestamp(collection.modified) 49 | return collections 50 | 51 | 52 | @debug_sync_request 53 | @logged_in_or_basicauth 54 | @weave_assert_version(("1.0", "1.1")) 55 | @weave_assert_username 56 | @csrf_exempt 57 | @weave_render_response 58 | def storage(request, version, username, timestamp, col_name=None, wboid=None): 59 | """ 60 | Handle storing Collections and WBOs. 61 | """ 62 | if 'X-If-Unmodified-Since' in request.META: 63 | since = datetime.fromtimestamp(float(request.META['X-If-Unmodified-Since'])) 64 | else: 65 | since = None 66 | 67 | if request.method == 'GET': 68 | # Returns a list of the WBO contained in a collection. 69 | collection = get_object_or_404(Collection.on_site, user=request.user, name=col_name) 70 | 71 | if wboid is not None: # return one WBO 72 | wbo = get_object_or_404(Wbo, user=request.user, collection=collection, wboid=wboid) 73 | return wbo.get_response_dict() 74 | 75 | wbo_queryset = Wbo.objects.filter(user=request.user, collection=collection) 76 | wbo_queryset = limit_wbo_queryset(request, wbo_queryset) 77 | 78 | # If defined, returns the full WBO, rather than just the id. 79 | if not 'full' in request.GET: # return only the WBO ids 80 | return [wbo.wboid for wbo in wbo_queryset] 81 | 82 | return [wbo.get_response_dict() for wbo in wbo_queryset] 83 | 84 | elif request.method == 'PUT': 85 | # https://wiki.mozilla.org/Labs/Weave/Sync/1.0/API#PUT 86 | # Adds the WBO defined in the request body to the collection. 87 | 88 | payload = request.raw_post_data 89 | logger.debug("Payload for PUT: %s" % payload) 90 | if not payload: 91 | raise NotImplementedError 92 | 93 | val = json.loads(payload) 94 | 95 | if val.get('id', None) != wboid: 96 | raise ValidationError 97 | 98 | # TODO: I don't think that it's good to just pass 0 in case the header is not defined 99 | collection, created = Collection.on_site.create_or_update( 100 | request.user, 101 | col_name, 102 | timestamp, 103 | since, 104 | ) 105 | 106 | wbo, created = Wbo.objects.create_or_update(val, collection, request.user, timestamp) 107 | 108 | return weave_timestamp(timestamp) 109 | 110 | elif request.method == 'POST': 111 | status = {'success': [], 'failed': []} 112 | payload = request.raw_post_data 113 | logger.debug("Payload in POST: %s" % payload) 114 | if not payload: 115 | raise NotImplementedError 116 | 117 | # TODO: I don't think that it's good to just pass 0 in case the header is not defined 118 | collection, created = Collection.on_site.create_or_update( 119 | request.user, 120 | col_name, 121 | timestamp, 122 | since, 123 | ) 124 | 125 | if created: 126 | logger.debug("Create new collection %s" % collection) 127 | else: 128 | logger.debug("Found existing collection %s" % collection) 129 | 130 | data = json.loads(payload) 131 | 132 | if not isinstance(data, list): 133 | raise NotImplementedError 134 | 135 | for item in data: 136 | try: 137 | wbo, created = Wbo.objects.create_or_update(item, collection, request.user, timestamp) 138 | except ValidationError, err: 139 | logger.debug("Can't create Wbo from %r: %s" % (item, err)) 140 | status['failed'].append(item["id"]) 141 | else: 142 | status['success'].append(wbo.wboid) 143 | 144 | if created: 145 | logger.debug("New wbo created: %s" % wbo) 146 | else: 147 | logger.debug("Existing wbo updated: %s" % wbo) 148 | 149 | return status 150 | elif request.method == 'DELETE': 151 | # FIXME: This is am mess, it needs better structure 152 | if col_name is None and wboid is None: 153 | Collection.on_site.filter(user=request.user).delete() 154 | return weave_timestamp(timestamp) 155 | 156 | if col_name is not None and wboid is not None: 157 | try: 158 | wbo = Wbo.objects.get(user=request.user, collection__name=col_name, wboid=wboid) 159 | except Wbo.DoesNotExist, err: 160 | msg = ( 161 | "Deletion of wboid %s from collection %r for user %s requested," 162 | " but there is no such wbo: %s" 163 | ) % (wboid, col_name, request.user, err) 164 | logger.info(msg) 165 | if settings.DEBUG: 166 | raise Http404(msg) 167 | else: 168 | raise Http404() 169 | else: 170 | logger.info("Delete Wbo %s in collection %s for user %s" % (wbo.wboid, col_name, request.user)) 171 | wbo.delete() 172 | else: 173 | ids = request.GET.get('ids', None) 174 | if ids is not None: 175 | for wbo in Wbo.objects.filter(user=request.user, wboid__in=ids.split(',')): 176 | logger.info("Delete Wbo %s in collection %s for user %s" % (wbo.wboid, col_name, request.user)) 177 | wbo.delete() 178 | else: 179 | collection = Collection.on_site.filter(user=request.user, name=col_name).delete() 180 | if collection is not None: 181 | logger.info("Delete collection %s for user %s" % (collection.name, request.user)) 182 | Wbo.objects.filter(user=request.user, collection=collection).delete() 183 | else: 184 | logger.info("Deletion of collection %s requested but there is no such collection!" % (col_name)) 185 | return weave_timestamp(timestamp) 186 | 187 | else: 188 | raise NotImplementedError("request.method %r not implemented!" % request.method) 189 | -------------------------------------------------------------------------------- /weave/views/user.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | ''' 4 | User handling for Weave. 5 | FIXME: Not complete yet. 6 | 7 | Created on 15.03.2010 8 | 9 | @license: GNU GPL v3 or above, see LICENSE for more details. 10 | @copyleft: 2010-2011 by the django-sync-server team, see AUTHORS for more details. 11 | ''' 12 | 13 | try: 14 | import json # New in Python v2.6 15 | except ImportError: 16 | from django.utils import simplejson as json 17 | 18 | from django.conf import settings 19 | from django.contrib.auth.models import User 20 | from django.core.exceptions import ImproperlyConfigured 21 | from django.core.urlresolvers import reverse 22 | from django.http import HttpResponseBadRequest, HttpResponse, \ 23 | HttpResponseNotFound, HttpResponseRedirect 24 | from django.views.decorators.csrf import csrf_exempt 25 | 26 | # django-sync-server own stuff 27 | from weave import Logging 28 | from weave import constants 29 | from weave.decorators import logged_in_or_basicauth, weave_assert_version, debug_sync_request 30 | from weave.utils import make_sync_hash 31 | 32 | logger = Logging.get_logger() 33 | 34 | 35 | @debug_sync_request 36 | @weave_assert_version(("1.0", "1.1")) 37 | @logged_in_or_basicauth 38 | @csrf_exempt 39 | def password(request): 40 | """ 41 | Change the user password. 42 | """ 43 | if request.method != 'POST': 44 | logger.error("wrong request method %r" % request.method) 45 | return HttpResponseBadRequest() 46 | 47 | # Make sure that we are able to change the password. 48 | # If for example django-auth-ldap is used for authentication it will set the password for 49 | # User objects to a unusable one in the database. Therefore we cannot change it, it has to 50 | # happen inside LDAP. 51 | if not request.user.has_usable_password(): 52 | logger.debug("Can't change password. User %s has a unusable password." % request.user.username) 53 | return HttpResponseBadRequest() 54 | 55 | # The PHP server for Weave uses the first 2048 (if there is enough data) characters 56 | # from POST data as the new password. We decided to throw an error if the password 57 | # data is longer than 256 characters. 58 | if len(request.raw_post_data) > 256: 59 | msg = ( 60 | "Don't change password for user %s." 61 | " POST data has more than 256 characters! (len=%i)" 62 | ) % (request.user.username, len(request.raw_post_data)) 63 | logger.debug(msg) 64 | return HttpResponseBadRequest() 65 | 66 | request.user.set_password(request.raw_post_data) 67 | request.user.save() 68 | logger.debug("Password for User %r changed to %r" % (request.user.username, request.raw_post_data)) 69 | return HttpResponse() 70 | 71 | 72 | def password_reset(request): 73 | """ 74 | Redirect to django own admin password change view. 75 | """ 76 | return HttpResponseRedirect(reverse('admin:password_change')) 77 | 78 | 79 | @debug_sync_request 80 | @weave_assert_version(("1.0", "1.1")) 81 | @csrf_exempt 82 | def node(request, version, username): 83 | """ 84 | finding cluster for user -> return 404 -> Using serverURL as data cluster (multi-cluster support disabled) 85 | """ 86 | try: 87 | User.objects.get(username=username) 88 | except User.DoesNotExist: 89 | logger.debug("User %r doesn't exist!" % username) 90 | return HttpResponseNotFound(constants.ERR_UID_OR_EMAIL_AVAILABLE) 91 | else: 92 | logger.debug("User %r exist." % username) 93 | #FIXME: Send the actual cluster URL instead of 404 94 | return HttpResponseNotFound(constants.ERR_UID_OR_EMAIL_IN_USE) 95 | 96 | 97 | @debug_sync_request 98 | @csrf_exempt 99 | def register_check(request, username): 100 | """ 101 | returns "1" if username is available (doesn't exist) 102 | https://wiki.mozilla.org/Labs/Weave/ServerAPI#Checking_if_Username.2FEmail_already_exists 103 | """ 104 | try: 105 | User.objects.get(username=username) 106 | except User.DoesNotExist: 107 | logger.debug("User %r doesn't exist!" % username) 108 | return HttpResponse(constants.ERR_UID_OR_EMAIL_AVAILABLE) 109 | else: 110 | logger.debug("User %r exist." % username) 111 | return HttpResponse(constants.ERR_UID_OR_EMAIL_IN_USE) 112 | 113 | 114 | @weave_assert_version(("1.0", "1.1")) 115 | @csrf_exempt 116 | def exists(request, version, username): 117 | """ 118 | https://wiki.mozilla.org/Labs/Weave/User/1.0/API#GET 119 | Returns 1 if the username is in use, 0 if it is available. 120 | 121 | e.g.: https://auth.services.mozilla.com/user/1/UserName 122 | """ 123 | if request.method == 'GET': 124 | try: 125 | User.objects.get(username=username) 126 | except User.DoesNotExist: 127 | logger.debug("User %r doesn't exist!" % username) 128 | return HttpResponse(constants.USER_DOES_NOT_EXIST) 129 | else: 130 | logger.debug("User %r exist." % username) 131 | return HttpResponse(constants.USER_EXIST) 132 | elif request.method == 'PUT': 133 | # Handle user creation. 134 | logger.debug("Raw post data: %r" % request.raw_post_data) 135 | data = json.loads(request.raw_post_data) 136 | 137 | if settings.WEAVE.DONT_USE_CAPTCHA == True: 138 | logger.warn("Create user without captcha. You should activate captcha!") 139 | else: 140 | # Check for aviability of recaptcha 141 | # (can be found at: http://pypi.python.org/pypi/recaptcha-client) 142 | try: 143 | from recaptcha.client.captcha import submit 144 | except ImportError: 145 | logger.error("Captcha requested but unable to import the 'recaptcha' package!") 146 | return HttpResponse("Captcha support disabled due to missing Python package 'recaptcha'.") 147 | if not getattr(settings.WEAVE, "RECAPTCHA_PRIVATE_KEY"): 148 | logger.error("Trying to create user but settings.WEAVE.RECAPTCHA_PRIVATE_KEY not set") 149 | raise ImproperlyConfigured 150 | 151 | result = submit( 152 | data['captcha-challenge'], 153 | data['captcha-response'], 154 | settings.WEAVE.RECAPTCHA_PRIVATE_KEY, 155 | request.META['REMOTE_ADDR'] 156 | ) 157 | if not result.is_valid: 158 | # Captcha failed. 159 | return HttpResponseBadRequest() 160 | 161 | if len(username) > 32 or len(data['password']) > 256: 162 | return HttpResponseBadRequest() 163 | 164 | email = data['email'] 165 | sync_hash = make_sync_hash(email) 166 | if not sync_hash == username: 167 | msg = "Error in sync hash: %r != %r" % (sync_hash, username) 168 | logger.error(msg) 169 | return HttpResponseBadRequest(msg) 170 | 171 | if len(username) <= 30: 172 | # Cut the sync sha1 hash to 30 characters, because 173 | # usernames are limited in django to a length of max. 30 chars. 174 | # http://docs.djangoproject.com/en/dev/topics/auth/#django.contrib.auth.models.User.username 175 | username = username[:30] 176 | 177 | try: 178 | user = User.objects.create_user(username, email, data['password']) 179 | except Exception, err: 180 | logger.error("Can't create user: %s" % err) 181 | else: 182 | logger.info("User %r with email %r created" % (user, email)) 183 | return HttpResponse() 184 | else: 185 | raise NotImplemented() 186 | --------------------------------------------------------------------------------