diff --git a/MANIFEST.in b/MANIFEST.in index 4c69487..aec55a3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,5 +6,4 @@ recursive-include linkdrop * recursive-include grinder * recursive-include docs * recursive-include tools * -recursive-include web * recursive-include wsgi * diff --git a/Makefile b/Makefile index 793ec3a..2ced17d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ BIN_DIR = bin endif APPNAME = server-shared-send -DEPS = server-share-core +DEPS = mozilla:server-core,github:server-share-core VIRTUALENV = virtualenv NOSE = $(BIN_DIR)/nosetests NOSETESTS_ARGS = -s diff --git a/README.md b/README.md index c9b9791..4f049e8 100644 --- a/README.md +++ b/README.md @@ -25,30 +25,58 @@ Some directory explanations: make build -If you get this error on Mac OS X: +If you are on OS X and you get errors or it does not work, see the OS X troubleshooting +section below. - /Developer/SDKs/MacOSX10.4u.sdk/usr/include/stdarg.h:4:25: error: stdarg.h: No such file or directory +### Start the virtualenv -It could be because the default version of GCC is too high. If you do + source bin/activate - ls -la /usr/bin/gcc +### Running f1 -And it points to gcc-4.2, then change it to point to gcc-4.0 (warning affects all gcc calls from then on): +Run the web server. 'reload' is useful for development, the webserver restarts on file changes, otherwise you can leave it off - sudo rm /usr/bin/gcc - sudo ln -s /usr/bin/gcc-4.0 /usr/bin/gcc + paster serve --reload development.ini -Info taken from [this web site](http://blog.coredumped.org/2009/09/snow-leopard-and-lxml.html) +Then visit: [http://127.0.0.1:5000/](http://127.0.0.1:5000/) for an index of api examples +## Troubleshooting OS X installs -### Running f1 +If the **make build** command produced errors or results in not being able to start +up the server, use the following steps. It is suggested you re-clone F1 before +doing the following steps, so that it starts out with a clean environment. -Run the web server. 'reload' is useful for development, the webserver restarts on file changes, otherwise you can leave it off +1. Make sure XCode 3 is installed. - paster serve --reload development.ini +2. Build your own version of Python: -Then visit: [http://127.0.0.1:5000/](http://127.0.0.1:5000/) for an index of api examples + sudo svn co http://svn.plone.org/svn/collective/buildout/python/ + sudo chown -R $USER ./python + cd python + vi buildout.cfg: then remove any references to python 2.4 and 2.5 + python bootstrap.py + ./bin/buildout + cd /usr/local/bin + sudo ln -s /opt/python/bin/virtualenv-2.6 virtualenv + +3. Now edit your .profile to make sure that if you have MacPorts installed, its PATH and MANPATH variables +are last in the list for those environment variables. + +I also removed export PYTHONPATH=/Users/aaa/hg/raindrop/server/python:$PYTHONPATH +and removed /Library/Frameworks/Python.framework/Versions/Current/bin from the $PATH variable. + +4. Build C libraries via Homebrew: + +Homebrew installs into /usr/local by default, and it is best if you chown the files in there to you: + + sudo chown -R $USER /usr/local + +If installed things before in these directories, remove these directories: /usr/local/include and /usr/local/lib + + ruby -e "$(curl -fsSLk https://gist.github.com/raw/323731/install_homebrew.rb)" + brew install memcached libmemcached +Then try the **make build** command above and continue from there. ## Setting up a valid Google domain for OpenID+OAuth diff --git a/build.py b/build.py index 2ca308b..417c76f 100644 --- a/build.py +++ b/build.py @@ -39,7 +39,8 @@ CURDIR = os.path.dirname(__file__) -REPO_ROOT = 'https://github.com/mozilla/%s.git' +REPOS = {'github': ('git', 'https://github.com/mozilla/%s.git'), + 'mozilla': ('hg', 'https://hg.mozilla.org/services/%s')} PYTHON = sys.executable @@ -74,21 +75,27 @@ def _envname(name): return name.upper().replace('-', '_') -def _update_cmd(project, latest_tags=False): +def _update_cmd(project, latest_tags=False, repo_type='git'): if latest_tags: - return 'git checkout -r "%s"' % get_latest_tag() + if repo_type == 'hg': + return 'hg up -r "%s"' % get_latest_tag() + else: + return 'git checkout -r "%s"' % get_latest_tag() else: - # looking for an environ with a specific tag or rev rev = os.environ.get(_envname(project)) if rev is not None: - if not verify_tag(rev): print('Unknown tag or revision: %s' % rev) sys.exit(1) - - return 'git checkout -r "%s"' % rev - return 'git checkout' + if repo_type == 'git': + return 'git checkout -r "%s"' % rev + else: + return 'hg up -r "%s"' % rev + if repo_type == 'git': + return 'git checkout' + else: + return 'hg up' def build_app(name, latest_tags, deps): @@ -113,16 +120,24 @@ def build_deps(deps, latest_tags): os.mkdir(deps_dir) for dep in deps: - repo = REPO_ROOT % dep - target = os.path.join(deps_dir, dep) + root, name = dep.split(':') + repo_type, repo_root = REPOS[root] + repo = repo_root % name + target = os.path.join(deps_dir, name) if os.path.exists(target): os.chdir(target) - _run('git pull') + if repo_type == 'git': + _run('git pull') + else: + _run('hg pull') else: - _run('git clone %s %s' % (repo, target)) - os.chdir(target) + if repo_type == 'git': + _run('git clone %s %s' % (repo, target)) + else: + _run('hg clone %s %s' % (repo, target)) - update_cmd = _update_cmd(dep, latest_tags) + os.chdir(target) + update_cmd = _update_cmd(dep, latest_tags, repo_type) _run(update_cmd) _run('%s setup.py develop' % PYTHON) finally: diff --git a/development.ini b/development.ini index ad487fb..0c0dea6 100644 --- a/development.ini +++ b/development.ini @@ -57,6 +57,10 @@ oauth.linkedin.com.request = https://api.linkedin.com/uas/oauth/requestToken oauth.linkedin.com.access = https://api.linkedin.com/uas/oauth/accessToken oauth.linkedin.com.authorize = https://api.linkedin.com/uas/oauth/authorize +sstatus.enabled = 0 +sstatus.servers = 127.0.0.1:11211 +sstatus.domains = google.com,twitter.com,facebook.com,linkedin.com + [server:main] use = egg:Paste#http host = 127.0.0.1 diff --git a/f1.spec.in b/f1.spec.in index ff3415d..a173120 100644 --- a/f1.spec.in +++ b/f1.spec.in @@ -17,7 +17,7 @@ %global python_sitelib /lib/python%{python_version}/site-packages %global python_sitearch /%{_lib}/python%{python_version}/site-packages -Name: %{f1_name_prefix}python%{pyver_sys} +Name: %{f1_name_prefix}python%{pyver} Version: %%version%% Release: 6%%git%%%{?dist} Summary: Share Links Fast. @@ -51,15 +51,13 @@ and love. F1 is made by Mozilla Messaging. %build export PYTHONPATH=$(pwd):%{f1_prefix}%{python_sitelib}:%{f1_prefix}%{python_sitearch} +mkdir web CFLAGS="%{optflags}" %{__python}%{pyver} setup.py build -make web PYTHON=%{__python}%{pyver} %install export PYTHONPATH=$(pwd):%{f1_prefix}%{python_sitelib}:%{f1_prefix}%{python_sitearch} rm -rf %{buildroot} %{__python}%{pyver} setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --prefix %{f1_prefix} --record=INSTALLED_FILES -%{__install} -m 755 -d %{buildroot}%{_var}/www/f1 -rsync -a web/ %{buildroot}%{_var}/www/f1/ %{__install} -m 755 -d %{buildroot}%{_sysconfdir}/f1 %{__install} -m 644 *.ini %{buildroot}%{_sysconfdir}/f1/ @@ -73,12 +71,13 @@ rsync -a web/ %{buildroot}%{_var}/www/f1/ rm -rf %{buildroot} %files -f INSTALLED_FILES -%{_var}/www/f1 %config(noreplace) %{_sysconfdir}/f1/*ini %defattr(-,root,root,-) %doc README.md LICENSE PKG-INFO docs/ %changelog +* Tue Apr 26 2011 Philippe M. Chiasson - 0.3.7dev-7 +- Remove web content, moved to mozilla-f1-web * Wed Apr 20 2011 Philippe M. Chiasson - 0.3.7dev-6 - Compile web content before packaging (make web) * Thu Apr 14 2011 Philippe M. Chiasson - 0.3.7dev-2 diff --git a/linkdrop/controllers/__init__.py b/linkdrop/controllers/__init__.py index e69de29..7ccb31b 100644 --- a/linkdrop/controllers/__init__.py +++ b/linkdrop/controllers/__init__.py @@ -0,0 +1,41 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Raindrop. +# +# The Initial Developer of the Original Code is +# Mozilla Messaging, Inc.. +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): Tarek Ziade +# + +# XXX services instance to be moved in the future application +# object once Pylons gets removed + +from linkoauth import Services +from pylons import config + +services = None + + +def get_services(): + global services + if services is None: + enabled = int(config.get('sstatus.ttl', '0')) + servers = config['sstatus.servers'].split(',') + domains = config['sstatus.domains'].split(',') + ttl = int(config.get('sstatus.ttl', '60')) + services = Services(domains, servers, ttl, enabled) + return services diff --git a/linkdrop/controllers/account.py b/linkdrop/controllers/account.py index 4e20bbf..15962ad 100644 --- a/linkdrop/controllers/account.py +++ b/linkdrop/controllers/account.py @@ -19,6 +19,7 @@ # the Initial Developer. All Rights Reserved. # # Contributor(s): +# Rob Miller (rmiller@mozilla.com) # import logging @@ -32,13 +33,12 @@ from pylons.controllers.util import redirect from pylons.controllers.core import HTTPException +from linkoauth.errors import AccessException +from linkdrop.controllers import get_services from linkdrop.lib.base import BaseController from linkdrop.lib.helpers import get_redirect_response from linkdrop.lib.metrics import metrics -from linkoauth import get_provider -from linkoauth.base import AccessException - log = logging.getLogger(__name__) @@ -66,19 +66,19 @@ def _create_account(self, domain, userid, username): def authorize(self, *args, **kw): provider = request.POST['domain'] log.info("authorize request for %r", provider) - service = get_provider(provider) - return service.responder().request_access(request, url, session) + services = get_services() + return services.request_access(provider, request, url, session) # this is not a rest api def verify(self, *args, **kw): provider = request.params.get('provider') log.info("verify request for %r", provider) - service = get_provider(provider) - auth = service.responder() acct = dict() try: - user = auth.verify(request, url, session) + services = get_services() + user = services.verify(provider, request, url, session) + account = user['profile']['accounts'][0] if (not user.get('oauth_token') and not user.get('oauth_token_secret')): diff --git a/linkdrop/controllers/contacts.py b/linkdrop/controllers/contacts.py index ed0feda..a0f3fb3 100644 --- a/linkdrop/controllers/contacts.py +++ b/linkdrop/controllers/contacts.py @@ -19,6 +19,7 @@ # the Initial Developer. All Rights Reserved. # # Contributor(s): +# Rob Miller (rmiller@mozilla.com) # import logging @@ -26,9 +27,10 @@ from pylons import request -from linkoauth import get_provider -from linkoauth.base import OAuthKeysException, ServiceUnavailableException +from linkoauth.errors import (OAuthKeysException, ServiceUnavailableException, + DomainNotRegisteredError) +from linkdrop.controllers import get_services from linkdrop.lib.base import BaseController from linkdrop.lib.helpers import json_exception_response, api_response from linkdrop.lib.helpers import api_entry, api_arg, get_passthrough_headers @@ -84,17 +86,8 @@ class ContactsController(BaseController): ], response={'type': 'object', 'doc': 'Portable Contacts Collection'}) def get(self, domain): - group = request.POST.get('group', None) - startIndex = int(request.POST.get('startindex', '0')) - maxResults = int(request.POST.get('maxresults', '25')) + page_data = request.POST.get('pageData', None) account_data = request.POST.get('account', None) - provider = get_provider(domain) - if provider is None: - error = { - 'message': "'domain' is invalid", - 'code': constants.INVALID_PARAMS, - } - return {'result': None, 'error': error} acct = None if account_data: @@ -109,11 +102,18 @@ def get(self, domain): return {'result': None, 'error': error} headers = get_passthrough_headers(request) + page_data = page_data and json.loads(page_data) or {} try: - result, error = provider.api(acct).getcontacts(startIndex, - maxResults, - group, - headers) + services = get_services() + result, error = services.getcontacts(domain, acct, page_data, + headers) + except DomainNotRegisteredError: + error = { + 'message': "'domain' is invalid", + 'code': constants.INVALID_PARAMS, + } + return {'result': None, 'error': error} + except OAuthKeysException, e: # more than likely we're missing oauth tokens for some reason. error = {'provider': domain, diff --git a/linkdrop/controllers/send.py b/linkdrop/controllers/send.py index 782d8cf..41e75ce 100644 --- a/linkdrop/controllers/send.py +++ b/linkdrop/controllers/send.py @@ -19,6 +19,7 @@ # the Initial Developer. All Rights Reserved. # # Contributor(s): +# Rob Miller (rmiller@mozilla.com) # import logging @@ -30,9 +31,10 @@ from pylons import request -from linkoauth import get_provider -from linkoauth.base import OAuthKeysException, ServiceUnavailableException +from linkoauth.errors import (OAuthKeysException, ServiceUnavailableException, + DomainNotRegisteredError) +from linkdrop.controllers import get_services from linkdrop.lib.base import BaseController from linkdrop.lib.helpers import json_exception_response, api_response from linkdrop.lib.helpers import api_entry, api_arg, get_passthrough_headers @@ -124,13 +126,6 @@ def send(self): 'code': constants.INVALID_PARAMS, } return {'result': result, 'error': error} - provider = get_provider(domain) - if provider is None: - error = { - 'message': "'domain' is invalid", - 'code': constants.INVALID_PARAMS, - } - return {'result': result, 'error': error} if account_data: acct = json.loads(account_data) @@ -161,11 +156,18 @@ def send(self): long_url=longurl, short_url=shorturl, acct_id=acct_hash) - # send the item. + # send the item headers = get_passthrough_headers(request) try: - result, error = provider.api(acct).sendmessage(message, args, - headers) + services = get_services() + result, error = services.sendmessage(domain, acct, message, + args, headers) + except DomainNotRegisteredError: + error = { + 'message': "'domain' is invalid", + 'code': constants.INVALID_PARAMS, + } + return {'result': result, 'error': error} except OAuthKeysException, e: # XXX - I doubt we really want a full exception logged here? #log.exception('error providing item to %s: %s', domain, e) diff --git a/linkdrop/tests/__init__.py b/linkdrop/tests/__init__.py index f4509e8..a4cd0df 100644 --- a/linkdrop/tests/__init__.py +++ b/linkdrop/tests/__init__.py @@ -16,8 +16,8 @@ __all__ = ['environ', 'url', 'TestController', 'testable_services'] -testable_services = ["google.com", "yahoo.com", "facebook.com", "twitter.com", - "linkedin.com"] +testable_services = ["google.com", "facebook.com", "twitter.com", + "linkedin.com", "yahoo.com"] # Invoke websetup with the current config file SetupCommand('setup-app').run( diff --git a/linkdrop/tests/controllers/test_account.py b/linkdrop/tests/controllers/test_account.py index 4ac07c8..63a0474 100644 --- a/linkdrop/tests/controllers/test_account.py +++ b/linkdrop/tests/controllers/test_account.py @@ -27,23 +27,25 @@ from mock import patch from nose import tools + class MockException(Exception): pass + class TestAccountController(TestController): def setUp(self): self.log_patcher = patch('linkdrop.controllers.account.log') self.req_patcher = patch('linkdrop.controllers.account.request') - self.gprov_patcher = patch('linkdrop.controllers.account.get_provider') + self.gserv_patcher = patch('linkdrop.controllers.account.get_services') self.log_patcher.start() self.req_patcher.start() - self.gprov_patcher.start() + self.gserv_patcher.start() self.controller = account.AccountController() def tearDown(self): self.log_patcher.stop() self.req_patcher.stop() - self.gprov_patcher.stop() + self.gserv_patcher.stop() def test_authorize(self): provider = 'example.com' @@ -51,10 +53,10 @@ def test_authorize(self): self.controller.authorize() logmsg = "authorize request for %r" account.log.info.assert_called_once_with(logmsg, provider) - account.get_provider.assert_called_once_with(provider) - mock_service = account.get_provider() - mock_service.responder().request_access.assert_called_once_with( - account.request, account.url, account.session) + account.get_services.assert_called_once() + mock_services = account.get_services() + mock_services.request_access.assert_called_once_with( + provider, account.request, account.url, account.session) @patch.dict('linkdrop.controllers.account.config', dict(oauth_failure='http://example.com/foo#bar', @@ -67,16 +69,16 @@ def test_verify(self, mock_redirect, mock_get_redirect_response, # first no oauth token -> verify failure provider = 'example.com' account.request.params = dict(provider=provider) - mock_service = account.get_provider() - mock_auth = mock_service.responder() + mock_services = account.get_services() mock_user = dict(profile={'accounts': (dict(),)},) - mock_auth.verify.return_value = mock_user + mock_services.verify.return_value = mock_user mock_resp = mock_get_redirect_response() mock_resp.exception = MockException() tools.assert_raises(MockException, self.controller.verify) - mock_auth.verify.assert_called_with(account.request, - account.url, - account.session) + mock_services.verify.assert_called_with(provider, + account.request, + account.url, + account.session) errmsg = 'error=Unable+to+get+OAUTH+access' mock_redirect.assert_called_with( 'http://example.com/foo?%s#bar' % errmsg) @@ -86,7 +88,7 @@ def test_verify(self, mock_redirect, mock_get_redirect_response, 'username': 'USERNAME'},)}, oauth_token=True, oauth_token_secret=False) - mock_auth.verify.return_value = mock_user + mock_services.verify.return_value = mock_user mock_redirect.reset_mock() tools.assert_raises(MockException, self.controller.verify) tools.eq_(mock_redirect.call_count, 0) @@ -100,20 +102,19 @@ def test_verify_access_exception(self, mock_redirect, mock_get_redirect_response): provider = 'example.com' account.request.params = dict(provider=provider) - mock_service = account.get_provider() - mock_auth = mock_service.responder() + mock_services = account.get_services() errmsg = 'ACCESSEXCEPTION' + def raise_access_exception(*args): - from linkoauth.base import AccessException + from linkoauth.errors import AccessException raise AccessException(errmsg) - mock_auth.verify.side_effect = raise_access_exception + mock_services.verify.side_effect = raise_access_exception mock_resp = mock_get_redirect_response() mock_resp.exception = MockException() tools.assert_raises(MockException, self.controller.verify) mock_redirect.assert_called_with( 'http://example.com/foo?error=%s#bar' % errmsg) - @patch.dict('linkdrop.controllers.account.config', dict(oauth_failure='http://example.com/foo#bar')) @patch('linkdrop.controllers.account.get_redirect_response') @@ -122,14 +123,14 @@ def test_verify_http_exception(self, mock_redirect, mock_get_redirect_response): provider = 'example.com' account.request.params = dict(provider=provider) - mock_service = account.get_provider() - mock_auth = mock_service.responder() + mock_services = account.get_services() from linkdrop.controllers.account import HTTPException url = 'http://example.com/redirect' exc = HTTPException(url, None) + def raise_http_exception(*args): raise exc - mock_auth.verify.side_effect = raise_http_exception + mock_services.verify.side_effect = raise_http_exception tools.assert_raises(HTTPException, self.controller.verify) errmsg = "account verification for %s caused a redirection: %s" account.log.info.assert_called_with(errmsg, provider, exc) diff --git a/linkdrop/tests/controllers/test_contacts.py b/linkdrop/tests/controllers/test_contacts.py index 3e54cf6..f752bea 100644 --- a/linkdrop/tests/controllers/test_contacts.py +++ b/linkdrop/tests/controllers/test_contacts.py @@ -28,15 +28,16 @@ from nose import tools import json + class TestContactsController(TestController): def setUp(self): self.req_patcher = patch('linkdrop.controllers.contacts.request') - self.gprov_patcher = patch( - 'linkdrop.controllers.contacts.get_provider') + self.gserv_patcher = patch( + 'linkdrop.controllers.contacts.get_services') self.metrics_patcher = patch( 'linkdrop.controllers.contacts.metrics') self.req_patcher.start() - self.gprov_patcher.start() + self.gserv_patcher.start() self.metrics_patcher.start() contacts.request.POST = dict() self.controller = contacts.ContactsController() @@ -44,15 +45,9 @@ def setUp(self): def tearDown(self): self.req_patcher.stop() - self.gprov_patcher.stop() + self.gserv_patcher.stop() self.metrics_patcher.stop() - def test_get_no_provider(self): - contacts.get_provider.return_value = None - res = self.real_get(self.controller, 'example.com') - tools.ok_(res['result'] is None) - tools.eq_(res['error']['message'], "'domain' is invalid") - def test_get_no_acct(self): domain = 'example.com' res = self.real_get(self.controller, domain) @@ -69,12 +64,25 @@ def _setup_acct_data(self): 'userid': 'USERID'}) contacts.request.POST['account'] = acct_json + def test_get_no_provider(self): + from linkoauth.errors import DomainNotRegisteredError + + def raise_domainnotregisterederror(*args): + raise DomainNotRegisteredError + mock_services = contacts.get_services() + mock_services.getcontacts.side_effect = raise_domainnotregisterederror + self._setup_acct_data() + res = self.real_get(self.controller, 'example.com') + tools.ok_(res['result'] is None) + tools.eq_(res['error']['message'], "'domain' is invalid") + def test_get_oauthkeysexception(self): - from linkoauth.base import OAuthKeysException + from linkoauth.errors import OAuthKeysException + def raise_oauthkeysexception(*args): raise OAuthKeysException('OAUTHKEYSEXCEPTION') - mock_getcontacts = contacts.get_provider().api().getcontacts - mock_getcontacts.side_effect = raise_oauthkeysexception + mock_services = contacts.get_services() + mock_services.getcontacts.side_effect = raise_oauthkeysexception domain = 'example.com' self._setup_acct_data() res = self.real_get(self.controller, domain) @@ -87,11 +95,12 @@ def raise_oauthkeysexception(*args): tools.ok_(res['error']['message'].startswith('not logged in')) def test_get_serviceunavailexception(self): - from linkoauth.base import ServiceUnavailableException + from linkoauth.errors import ServiceUnavailableException + def raise_servunavailexception(*args): raise ServiceUnavailableException('SERVUNAVAIL') - mock_getcontacts = contacts.get_provider().api().getcontacts - mock_getcontacts.side_effect = raise_servunavailexception + mock_services = contacts.get_services() + mock_services.getcontacts.side_effect = raise_servunavailexception domain = 'example.com' self._setup_acct_data() res = self.real_get(self.controller, domain) @@ -107,8 +116,8 @@ def raise_servunavailexception(*args): def test_get_success(self): domain = 'example.com' self._setup_acct_data() - mock_getcontacts = contacts.get_provider().api().getcontacts - mock_getcontacts.return_value = ('SUCCESS', None) + mock_services = contacts.get_services() + mock_services.getcontacts.return_value = ('SUCCESS', None) res = self.real_get(self.controller, domain) tools.eq_(res['result'], 'SUCCESS') tools.ok_(res['error'] is None) diff --git a/linkdrop/tests/controllers/test_send.py b/linkdrop/tests/controllers/test_send.py index a521929..f301b21 100644 --- a/linkdrop/tests/controllers/test_send.py +++ b/linkdrop/tests/controllers/test_send.py @@ -29,6 +29,7 @@ import hashlib import json + class TestSendController(TestController): domain = 'example.com' username = 'USERNAME' @@ -36,20 +37,21 @@ class TestSendController(TestController): def setUp(self): self.req_patcher = patch('linkdrop.controllers.send.request') - self.gprov_patcher = patch( - 'linkdrop.controllers.send.get_provider') + self.gserv_patcher = patch( + 'linkdrop.controllers.send.get_services') self.metrics_patcher = patch( 'linkdrop.controllers.send.metrics') self.req_patcher.start() - self.gprov_patcher.start() + self.gserv_patcher.start() self.metrics_patcher.start() send.request.POST = dict() self.controller = send.SendController() self.real_send = self.controller.send.undecorated.undecorated + self.mock_sendmessage = send.get_services().sendmessage def tearDown(self): self.req_patcher.stop() - self.gprov_patcher.stop() + self.gserv_patcher.stop() self.metrics_patcher.stop() def _setup_domain(self, domain=None): @@ -76,8 +78,7 @@ def _setup_provider(self, result=None, error=None): result = dict(result='success', domain=self.domain) self.result = result self.error = error - provider = send.get_provider() - provider.api().sendmessage.return_value = result, error + self.mock_sendmessage.return_value = result, error def test_send_no_domain(self): res = self.real_send(self.controller) @@ -86,9 +87,13 @@ def test_send_no_domain(self): def test_send_no_provider(self): self._setup_domain() - send.get_provider.return_value = None + self._setup_acct_data() + from linkoauth.errors import DomainNotRegisteredError + + def raise_domainnotregisterederror(*args): + raise DomainNotRegisteredError + self.mock_sendmessage.side_effect = raise_domainnotregisterederror res = self.real_send(self.controller) - send.get_provider.assert_called_once_with(self.domain) tools.eq_(res['result'], dict()) tools.eq_(res['error']['message'], "'domain' is invalid") @@ -114,12 +119,12 @@ def test_send_shorten(self, mock_shorten): shorturl = 'http://sh.ort/url' mock_shorten.return_value = shorturl res = self.real_send(self.controller) - mock_shorten.assert_called_once_with('http://'+longurl) + mock_shorten.assert_called_once_with('http://' + longurl) timer_args = send.metrics.start_timer.call_args_list tools.eq_(len(timer_args), 2) tools.eq_(timer_args[0], ((send.request,), dict(long_url=longurl))) tools.eq_(timer_args[1][0][0], send.request) - tools.eq_(timer_args[1][1]['long_url'], 'http://'+longurl) + tools.eq_(timer_args[1][1]['long_url'], 'http://' + longurl) tools.eq_(timer_args[1][1]['short_url'], shorturl) tools.eq_(timer_args[1][1]['acct_id'], self._acct_hash()) mock_timer = send.metrics.start_timer() @@ -132,11 +137,11 @@ def test_send_shorten(self, mock_shorten): tools.eq_(res['result']['shorturl'], shorturl) def test_send_oauthkeysexception(self): - from linkoauth.base import OAuthKeysException + from linkoauth.errors import OAuthKeysException + def raise_oauthkeysexception(*args): raise OAuthKeysException('OAUTHKEYSEXCEPTION') - mock_sendmessage = send.get_provider().api().sendmessage - mock_sendmessage.side_effect = raise_oauthkeysexception + self.mock_sendmessage.side_effect = raise_oauthkeysexception self._setup_domain() self._setup_acct_data() res = self.real_send(self.controller) @@ -150,14 +155,14 @@ def raise_oauthkeysexception(*args): tools.eq_(res['error']['status'], 401) def test_send_serviceunavailexception(self): - from linkoauth.base import ServiceUnavailableException + from linkoauth.errors import ServiceUnavailableException debug_msg = 'DEBUG' + def raise_servunavailexception(*args): e = ServiceUnavailableException('SERVUNAVAIL') e.debug_message = debug_msg raise e - mock_sendmessage = send.get_provider().api().sendmessage - mock_sendmessage.side_effect = raise_servunavailexception + self.mock_sendmessage.side_effect = raise_servunavailexception self._setup_domain() self._setup_acct_data() res = self.real_send(self.controller) @@ -174,9 +179,8 @@ def raise_servunavailexception(*args): def test_send_error(self): self._setup_domain() self._setup_acct_data() - mock_sendmessage = send.get_provider().api().sendmessage errmsg = 'ERROR' - mock_sendmessage.return_value = (dict(), errmsg) + self.mock_sendmessage.return_value = (dict(), errmsg) res = self.real_send(self.controller) mock_timer = send.metrics.start_timer() mock_timer.track.assert_called_with('send-error', error=errmsg) @@ -188,8 +192,7 @@ def test_send_success(self): self._setup_acct_data() to_ = 'hueylewis@example.com' send.request.POST['to'] = to_ - mock_sendmessage = send.get_provider().api().sendmessage - mock_sendmessage.return_value = ( + self.mock_sendmessage.return_value = ( dict(message='SUCCESS'), dict()) res = self.real_send(self.controller) mock_timer = send.metrics.start_timer() diff --git a/linkdrop/tests/lib/test_helpers.py b/linkdrop/tests/lib/test_helpers.py index caa61e5..e71a2d6 100644 --- a/linkdrop/tests/lib/test_helpers.py +++ b/linkdrop/tests/lib/test_helpers.py @@ -65,6 +65,7 @@ def test_json_exception_response(self, mock_metrics, mock_log): # first make sure HTTPException gets passed through from webob.exc import HTTPException http_exc = HTTPException('msg', 'wsgi_response') + @helpers.json_exception_response def http_exception_raiser(): raise http_exc @@ -87,6 +88,7 @@ def other_exception_raiser(): @patch('linkdrop.lib.helpers.get_pylons') def test_api_response(self, mock_get_pylons): data = {'foo': 'bar', 'baz': 'bawlp'} + @helpers.api_response def sample_data(): return data @@ -117,6 +119,7 @@ def sample_data(): # xml format / list data = ['foo', 'bar', 'baz', 'bawlp'] + @helpers.api_response def sample_data2(): return data @@ -145,8 +148,7 @@ def test_api_entry(self): 'BodyArg2 Doc'), ], response={'type': 'list', 'doc': 'callargs list'}, - name='NAME' - ) + name='NAME') def api_fn(arg1, arg2, kwarg1=None, kwarg2=None): return [(arg1, arg2), dict(kwarg1=kwarg1, kwarg2=kwarg2)] res = api_fn(1, 2, kwarg1='foo', kwarg2='bar') diff --git a/linkdrop/tests/lib/test_metrics.py b/linkdrop/tests/lib/test_metrics.py index bdc554e..0823fe0 100644 --- a/linkdrop/tests/lib/test_metrics.py +++ b/linkdrop/tests/lib/test_metrics.py @@ -3,12 +3,14 @@ from mock import Mock from nose import tools + class TestMetricsConsumer(TestController): @tools.raises(NotImplementedError) def test_consume_raises_notimplemented(self): mc = metrics.MetricsConsumer() mc.consume('somedata') + class TestMetricsCollector(TestController): def setUp(self): self.consumer = Mock() diff --git a/linkdrop/tests/lib/test_shortener.py b/linkdrop/tests/lib/test_shortener.py index 3224443..5fdad50 100644 --- a/linkdrop/tests/lib/test_shortener.py +++ b/linkdrop/tests/lib/test_shortener.py @@ -4,6 +4,7 @@ from nose import tools import json + @patch('linkdrop.lib.shortener.log') @patch('linkdrop.lib.shortener.urllib') def test_shorten_link_bad_response(mock_urllib, mock_log): @@ -18,6 +19,7 @@ def test_shorten_link_bad_response(mock_urllib, mock_log): mock_log.error.assert_called_once_with( "unexpected bitly response: %r", shortener_response) + @patch('linkdrop.lib.shortener.urllib') def test_shorten_link(mock_urllib): longurl = 'http://example.com/long/long/really/no/i/mean/really/long/url' @@ -29,4 +31,3 @@ def test_shorten_link(mock_urllib): mock_urllib.urlopen.assert_called_once() urlopen_arg = mock_urllib.urlopen.call_args[0][0] tools.ok_('longUrl=%s' % longurl in urlopen_arg) - diff --git a/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/expected-f1-data.json b/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/expected-f1-data.json index dc6404f..456a131 100644 --- a/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/expected-f1-data.json +++ b/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/expected-f1-data.json @@ -19,9 +19,6 @@ } ] } - ], - "itemsPerPage": 1, - "startIndex": 0, - "totalResults": 1 + ] } } diff --git a/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/meta.json b/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/meta.json index dc11c25..42fe2a5 100644 --- a/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/meta.json +++ b/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/meta.json @@ -1,5 +1,5 @@ { "protocol": "http", "reason": "automatic success save", - "uri": "http://api.linkedin.com/v1/people/~/connections?count=500" + "uri": "http://api.linkedin.com/v1/people/~/connections?count=25" } diff --git a/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/request-0 b/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/request-0 index 4df0e1a..e52eb13 100644 --- a/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/request-0 +++ b/linkdrop/tests/services/corpus/http-api.linkedin.com-contacts-successful/request-0 @@ -1,4 +1,4 @@ -GET /v1/people/~/connections?count=500 +GET /v1/people/~/connections?count=25 x-li-format: json accept-encoding: gzip, deflate user-agent: Python-httplib2/$Rev$ diff --git a/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/request-0 b/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/request-0 index a89350f..4f90d28 100644 --- a/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/request-0 +++ b/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/request-0 @@ -1,4 +1,4 @@ -GET /1/statuses/followers.json?screen_name=mytwitterid +GET /1/statuses/followers.json?screen_name=mytwitterid&cursor=-1 accept-encoding: gzip, deflate user-agent: Python-httplib2/$Rev$ diff --git a/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/response-0 b/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/response-0 index 5caac76..e125ff6 100644 --- a/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/response-0 +++ b/linkdrop/tests/services/corpus/http-api.twitter.com-contacts-successful/response-0 @@ -20,7 +20,8 @@ x-ratelimit-limit: 350 content-type: application/json; charset=utf-8 x-ratelimit-reset: 1302676561 -[ +{ + "users": [ { "follow_request_sent":false, "profile_background_image_url":"http:\/\/a3.twimg.com\/a\/1302541726\/images\/themes\/theme1\/bg.png", @@ -99,4 +100,9 @@ x-ratelimit-reset: 1302676561 "utc_offset":36000, "profile_background_color":"C0DEED" } -] + ], + "next_cursor":0, + "previous_cursor":0, + "next_cursor_str":"0", + "previous_cursor_str":"0" +} diff --git a/linkdrop/tests/services/corpus/http-graph.facebook.com-contacts-list-no-groups/expected-f1-data.json b/linkdrop/tests/services/corpus/http-graph.facebook.com-contacts-list-no-groups/expected-f1-data.json index 6aeeff2..af415cb 100644 --- a/linkdrop/tests/services/corpus/http-graph.facebook.com-contacts-list-no-groups/expected-f1-data.json +++ b/linkdrop/tests/services/corpus/http-graph.facebook.com-contacts-list-no-groups/expected-f1-data.json @@ -3,7 +3,6 @@ "result": { "entry": [], "itemsPerPage": 0, - "startIndex": 0, - "totalResults": 0 + "startIndex": 0 } } diff --git a/linkdrop/tests/services/corpus/http-social.yahooapis.com-contacts-success/expected-f1-data.json b/linkdrop/tests/services/corpus/http-social.yahooapis.com-contacts-success/expected-f1-data.json index 83c055f..69be310 100644 --- a/linkdrop/tests/services/corpus/http-social.yahooapis.com-contacts-success/expected-f1-data.json +++ b/linkdrop/tests/services/corpus/http-social.yahooapis.com-contacts-success/expected-f1-data.json @@ -12,9 +12,6 @@ ], "nickname": "test" } - ], - "itemsPerPage": 1, - "startIndex": 0, - "totalResults": 1 + ] } } diff --git a/linkdrop/tests/services/test_playback.py b/linkdrop/tests/services/test_playback.py index c5cdd9a..35a894a 100644 --- a/linkdrop/tests/services/test_playback.py +++ b/linkdrop/tests/services/test_playback.py @@ -79,6 +79,7 @@ def save_capture(self, reason=""): class HttpReplayer(ProtocolReplayer): to_playback = [] + def request(self, uri, method="GET", body=None, headers=None, **kw): freq, fresp = self.to_playback.pop(0) if freq is not None: @@ -96,12 +97,14 @@ def request(self, uri, method="GET", body=None, headers=None, **kw): if n.lower() == "content-type": break else: - headers['Content-Type'] = "application/x-www-form-urlencoded" + headers['Content-Type'] = ("application/" + "x-www-form-urlencoded") # We may wind up with the oauth stuff creating an 'Authorization' # header as unicode. Force all headers back to a string and if # any blow up as being non-ascii, we have a deeper problem... gotheadersstr = "\r\n".join( - ["%s: %s" % (n, v.encode("ascii")) for n, v in headers.iteritems()]) + ["%s: %s" % (n, v.encode("ascii")) + for n, v in headers.iteritems()]) bodystr = gotheadersstr + "\r\n\r\n" + (body or '') gotob = email.message_from_string(bodystr) if headers: @@ -115,7 +118,7 @@ def request(self, uri, method="GET", body=None, headers=None, **kw): if hname.lower() in ["content-length", "content-type", "authorization"]: continue - # otherwise the header must match exactly. + # otherwise the header must match exactly. eq_(hval, reqob[hname]) # finally check the content (ie, the body) is as expected. assert_messages_equal(gotob, reqob) @@ -127,7 +130,7 @@ def request(self, uri, method="GET", body=None, headers=None, **kw): return httplib2.Response(resp), content -from linkoauth.google_ import SMTP +from linkoauth.backends.google_ import SMTP class SmtpReplayer(SMTP, ProtocolReplayer): @@ -403,41 +406,40 @@ def getDefaultRequest(self, req_type): def setupReplayers(httpReplayer=HttpReplayer, smtpReplayer=SmtpReplayer): - import linkoauth.facebook_ - linkoauth.facebook_.HttpRequestor = httpReplayer - import linkoauth.yahoo_ - linkoauth.yahoo_.HttpRequestor = httpReplayer - import linkoauth.google_ - linkoauth.google_.SMTPRequestor = smtpReplayer - linkoauth.google_.OAuth2Requestor = httpReplayer - import linkoauth.twitter_ - linkoauth.twitter_.OAuth2Requestor = httpReplayer - import linkoauth.linkedin_ - linkoauth.linkedin_.OAuth2Requestor = httpReplayer - import linkoauth.base - linkoauth.base.HttpRequestor = httpReplayer - httpReplayer.to_playback = [] - smtpReplayer.to_playback = None + from linkoauth.backends import facebook_ + facebook_.HttpRequestor = httpReplayer + from linkoauth.backends import yahoo_ + yahoo_.HttpRequestor = httpReplayer + from linkoauth.backends import google_ + google_.SMTPRequestor = smtpReplayer + google_.OAuth2Requestor = httpReplayer + from linkoauth.backends import twitter_ + twitter_.OAuth2Requestor = httpReplayer + from linkoauth.backends import linkedin_ + linkedin_.OAuth2Requestor = httpReplayer + import linkoauth.oauth + linkoauth.oauth.HttpRequestor = httpReplayer + HttpReplayer.to_playback = [] + SmtpReplayer.to_playback = None def teardownReplayers(): assert not HttpReplayer.to_playback, HttpReplayer.to_playback assert not SmtpReplayer.to_playback, SmtpReplayer.to_playback import linkoauth.protocap - import linkoauth.facebook_ - linkoauth.facebook_.HttpRequestor = linkoauth.protocap.HttpRequestor - import linkoauth.yahoo_ - linkoauth.yahoo_.HttpRequestor = linkoauth.protocap.HttpRequestor - import linkoauth.protocap - import linkoauth.google_ - linkoauth.google_.SMTPRequestor = linkoauth.google_.SMTPRequestorImpl - linkoauth.google_.OAuth2Requestor = linkoauth.protocap.OAuth2Requestor - import linkoauth.twitter_ - linkoauth.twitter_.OAuth2Requestor = linkoauth.protocap.OAuth2Requestor - import linkoauth.linkedin_ - linkoauth.linkedin_.OAuth2Requestor = linkoauth.protocap.OAuth2Requestor - import linkoauth.base - linkoauth.base.HttpRequestor = linkoauth.protocap.HttpRequestor + from linkoauth.backends import facebook_ + facebook_.HttpRequestor = linkoauth.protocap.HttpRequestor + from linkoauth.backends import yahoo_ + yahoo_.HttpRequestor = linkoauth.protocap.HttpRequestor + from linkoauth.backends import google_ + google_.SMTPRequestor = google_.SMTPRequestorImpl + google_.OAuth2Requestor = linkoauth.protocap.OAuth2Requestor + from linkoauth.backends import twitter_ + twitter_.OAuth2Requestor = linkoauth.protocap.OAuth2Requestor + from linkoauth.backends import linkedin_ + linkedin_.OAuth2Requestor = linkoauth.protocap.OAuth2Requestor + from linkoauth import oauth + oauth.HttpRequestor = linkoauth.protocap.HttpRequestor host_to_domain = { @@ -510,6 +512,7 @@ def runOne(canned): # % nosetests ... linkdrop/tests/services/test_playback.py:test_service_replay_http_www_google_com_auth_successful # to just run one specific test from the corpus. for canned in genCanned(): + @with_setup(setupReplayers, teardownReplayers) def decoratedRunOne(canned=canned): runOne(canned) diff --git a/linkdrop/websetup.py b/linkdrop/websetup.py index 9a2e35c..44c9d31 100644 --- a/linkdrop/websetup.py +++ b/linkdrop/websetup.py @@ -34,5 +34,5 @@ def setup_app(command, conf, vars): """Place any commands to setup linkdrop here""" # Don't reload the app if it was loaded under the testing environment - if not pylons.test.pylonsapp: # pragma: no cover + if not pylons.test.pylonsapp: # pragma: no cover load_environment(conf.global_conf, conf.local_conf) diff --git a/production.ini b/production.ini index bc85a5a..4d72838 100644 --- a/production.ini +++ b/production.ini @@ -32,10 +32,12 @@ oauth.google.com.scope = https://mail.google.com/ http://www.google.com/m8/feeds use = egg:Paste#http host = 0.0.0.0 port = 5000 +workers = 10 +worker_class = gevent [filter-app:main] use = egg:Beaker#beaker_session -next = csrf +next = sessioned beaker.session.key = linkdrop beaker.session.secret = TMShmttWBvOMws810dW2nFB7k beaker.session.cookie_expires = false @@ -57,24 +59,22 @@ beaker.cache.data_dir = %(here)s/data/cache beaker.session.data_dir = %(here)s/data/sessions beaker.session.lock_dir = %(here)s/data/sessions/lock -[filter-app:csrf] -use = egg:linkdrop#csrf -# allow access to account api's for oauth, which will not have csrf token -# be sure to use the FULL path -csrf.unprotected_path = /account -next = api - -[composite:sessioned] +[app:sessioned] use = egg:Paste#urlmap / = home /api = api [app:home] -use = egg:Paste#static -document_root = %(here)s/web +use = egg:linkdrop#static +document_root = /var/www/f1 + +[filter:proxy-prefix] +use = egg:PasteDeploy#prefix +prefix = /api [app:api] use = egg:linkdrop +filter-with = proxy-prefix full_stack = true static_files = false session_middleware = false @@ -128,10 +128,11 @@ formatter = generic [handler_file] class = FileHandler -args = ('%(here)s/linkdrop.log', 'a') +args = ('/var/log/linkdrop.log', 'a') level = INFO formatter = generic [formatter_generic] format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s datefmt = %H:%M:%S + diff --git a/test.ini b/test.ini index 29ee8ad..14c8853 100644 --- a/test.ini +++ b/test.ini @@ -37,6 +37,9 @@ test_shortener = yes # We specify our local test server is used for all service connections. stub_service_server=127.0.0.1:8327 +oauth_failure = /dev/auth.html#oauth_failure +oauth_success = /dev/auth.html#oauth_success + # Note we do *not* use valid keys and secrets here as the automated tests # must not hit the service directly. # See comments in main .ini file for additional comments about these. @@ -73,6 +76,10 @@ oauth.linkedin.com.request = https://api.linkedin.com/uas/oauth/requestToken oauth.linkedin.com.access = https://api.linkedin.com/uas/oauth/accessToken oauth.linkedin.com.authorize = https://api.linkedin.com/uas/oauth/authorize +sstatus.enabled = 0 +sstatus.servers = 127.0.0.1:11211 +sstatus.domains = google.com,twitter.com,facebook.com,linkedin.com + [server:main] use = egg:Paste#http host = 127.0.0.1 diff --git a/web/dev/1/scripts/Contacts.js b/web/dev/1/scripts/Contacts.js index 136e893..c7ee059 100644 --- a/web/dev/1/scripts/Contacts.js +++ b/web/dev/1/scripts/Contacts.js @@ -129,36 +129,49 @@ function ($, object, fn, dispatch, rdapi, accounts) { })); }, - fetch: function () { + fetch: function (pageData) { var acct = this.svcAccount; accounts.getService(acct.domain, acct.userid, acct.username, fn.bind(this, function (svcData) { + + var data = { + username: acct.username, + userid: acct.userid, + account: JSON.stringify(svcData) + }; + if (pageData) { + data.pageData = JSON.stringify(pageData); + } rdapi('contacts/' + acct.domain, { type: 'POST', - data: { - username: acct.username, - userid: acct.userid, - startindex: 0, - maxresults: 500, - account: JSON.stringify(svcData) - }, + data: data, //Only wait for 10 seconds, then give up. timeout: 10000, success: fn.bind(this, function (json) { //Transform data to a form usable by the front end. if (json && !json.error) { var entries = json.result.entry; + var nextPageData = json.result.pageData; this.getFormattedContacts(entries, fn.bind(this, function (contacts) { - this.contacts = contacts; + if (!pageData) { + this.contacts = contacts; + } else { + this.contacts.push.apply(this.contacts, contacts); + } this.lastUpdated = (new Date()).getTime(); this.toStore({ list: this.contacts }); + if (nextPageData) { + setTimeout(fn.bind(this, function(nextPageData) { + this.fetch(nextPageData); + }), 1, nextPageData); + } }) ); } diff --git a/web/dev/1/scripts/Select.css b/web/dev/1/scripts/Select.css index e834e31..ee24a2b 100644 --- a/web/dev/1/scripts/Select.css +++ b/web/dev/1/scripts/Select.css @@ -5,18 +5,20 @@ .Select { position:relative; display: inline-block; - overflow: hidden; padding-right: 15px; z-index: 100; - border: 1px solid #A6AFB6; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15) inset; + border: 1px solid #303030; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15); height: 24px; cursor: pointer; - background-color: white; + color: #e2e2e2; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #767676), color-stop(100%, #545454)); + background-image: -moz-linear-gradient(center top , #767676 0%, #545454 100%); + text-align: left; + border-radius: 4px; } .Select.open { - overflow: visible; padding-right: 0; border: 0; } @@ -24,11 +26,13 @@ .Select ul { list-style: none; z-index: 100; - background-color: white; } .Select.open ul { position: absolute; + top: -30px; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #767676), color-stop(100%, #545454)); + background-image: -moz-linear-gradient(center top , #767676 0%, #545454 100%); border: 1px solid gray; } @@ -59,7 +63,7 @@ .Select.open li.selected:hover, .Select.open li:hover { color:white; - background-color: #535F6D; + background-color: #2b2b2b; } .Select .triangle { @@ -71,8 +75,6 @@ line-height: 24px; padding: 0 2px; z-index: 101; - color: #A6AFB6; - background-color: white; background-image: url("/share/i/sprite.png"); background-position: center -362px; background-repeat: no-repeat; diff --git a/web/dev/1/scripts/fakeStorage.js b/web/dev/1/scripts/fakeStorage.js new file mode 100644 index 0000000..36e1b2d --- /dev/null +++ b/web/dev/1/scripts/fakeStorage.js @@ -0,0 +1,133 @@ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Raindrop. + * + * The Initial Developer of the Original Code is + * Mozilla Messaging, Inc.. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * */ + +/*jslint indent: 2 */ +/*global define: false, localStorage: false, location: false, console: false, + window: false */ +"use strict"; + +/** + * This module sets up a localStorage storage provider, to allow dev/testing + * the UI without needing the chrome storage. It is only loaded if the + * page that includes it explicitly loads it, currently done by setting + * the URL fragment ID to #test. + */ + +define(['dispatch'], function (dispatch) { + + var sub = dispatch.sub, + dataStore = (localStorage.chromeTestStore && + JSON.parse(localStorage.chromeTestStore)) || {}, + origin = location.protocol + "//" + location.host, + subs; + + // Helpers that deal with the target window subscriptions. + function targetPub(topic, data) { + dispatch.pub(topic, data); + } + + function saveStore() { + localStorage.chromeTestStore = JSON.stringify(dataStore); + } + + subs = { + panelReady: function () { + targetPub('shareState', { + status: 0, + open: true, + options: { + version: '0.7.2', + title: 'Firefox web browser', + description: 'All about firefox', + medium: null, + source: null, + url: 'http://www.mozilla.com/en-US/firefox/fx/', + canonicalUrl: null, + shortUrl: null, + previews: [{ + http_url: 'http://mozcom-cdn.mozilla.net/img/firefox-100.jpg' + }], + siteName: '', + prefs: { + system: 'dev', + bookmarking: true, + use_accel_key: true + } + } + }); + }, + + storeGet: function (key) { + var value = dataStore[key]; + //JSON wants null. + if (value === undefined) { + value = null; + } + targetPub('storeGetReturn', { + key: key, + value: value + }); + }, + + storeSet: function (data) { + dataStore[data.key] = data.value; + saveStore(); + targetPub('storeNotifyChange', { + key: data.key, + value: data.value + }); + }, + + storeRemove: function (key) { + delete dataStore[key]; + saveStore(); + targetPub('storeNotifyChange', { + key: key, + value: null + }); + } + }; + + // register all events. + window.addEventListener('message', function (evt) { + if (evt.origin === origin) { + var message; + try { + message = JSON.parse(evt.data); + } catch (e) { + console.error('Could not JSON parse: ' + evt.data); + } + + if (message && message.topic) { + if (subs[message.topic]) { + subs[message.topic](message.data); + } else { + // actually quite a few of these, uncomment if you want a play + // by play of topics going through the window. + //console.log("Unhandled topic: " + message.topic, message.data); + } + } + } + }, false); + +}); diff --git a/web/dev/1/scripts/services.js b/web/dev/1/scripts/services.js index 5e6467e..39a5e54 100644 --- a/web/dev/1/scripts/services.js +++ b/web/dev/1/scripts/services.js @@ -105,10 +105,10 @@ function (object) { }, shareTypes: [{ type: 'public', - name: 'public' + name: 'Public timeline' }, { type: 'direct', - name: 'direct message', + name: 'Direct Message', showTo: true, toLabel: 'type in name of recipient' }], @@ -173,7 +173,7 @@ function (object) { name: 'pape_max_auth_age', value: 0 } - }), + }) /*, Commenting out google apps, yahoo and linked in for now 'googleapps.com': new EmailSvcBase('Google Apps', { shareTypes: [{ type: 'direct', @@ -238,7 +238,7 @@ function (object) { 'widgets/AccountPanel': 'widgets/AccountPanelLinkedIn', 'Contacts': 'ContactsLinkedIn' } - }) + }) */ }, domainList: [], diff --git a/web/dev/1/scripts/shareOptions.js b/web/dev/1/scripts/shareOptions.js index f6ec2e0..889865a 100644 --- a/web/dev/1/scripts/shareOptions.js +++ b/web/dev/1/scripts/shareOptions.js @@ -51,16 +51,10 @@ define(['blade/url'], function (url) { } } - options.prefs = options.prefs || {}; - if (!options.title) { options.title = options.url; } - if (!options.prefs.system) { - options.prefs.system = 'prod'; - } - source = options.source; //If the source is larger than ~4KB then it will exceed the GET size diff --git a/web/dev/1/scripts/storage.js b/web/dev/1/scripts/storage.js index 3d801db..9fbc2f8 100644 --- a/web/dev/1/scripts/storage.js +++ b/web/dev/1/scripts/storage.js @@ -30,13 +30,6 @@ define(['dispatch'], function (dispatch) { callbacks = {}, store; - // Temporary workaround to allow separate tab of settings to still have - // access to the chrome storage. Not a good idea to do long term. - if (opener && !opener.closed && opener.require && - (store = opener.require('storage'))) { - return store; - } - store = { get: function (key, callback) { var keyCallbacks; diff --git a/web/dev/1/settings/index.html b/web/dev/1/settings/index.html index 0e25102..8dbccad 100644 --- a/web/dev/1/settings/index.html +++ b/web/dev/1/settings/index.html @@ -1,7 +1,7 @@ - Mozilla F1: Configuration + Share: Configuration @@ -9,168 +9,58 @@ - - + + + -Feedback - -
- - +
+ + +
- -
    -
  • accounts
  • - - -
- -
- -
-

My accounts

- - -

Add accounts

- -
- - - - -
- -
-
- +
+
- - -
-
- -
- We care about your privacy, seriously. -
-
-
- -
- - + - + - + diff --git a/web/dev/1/settings/index.js b/web/dev/1/settings/index.js index 2724e85..273606d 100644 --- a/web/dev/1/settings/index.js +++ b/web/dev/1/settings/index.js @@ -27,15 +27,13 @@ "use strict"; define([ "require", "jquery", "blade/fn", "rdapi", "oauth", "blade/jig", - "dispatch", "storage", "accounts", "dotCompare", "blade/url", - "services", "placeholder", "jquery.colorFade", "jquery.textOverflow"], + "dispatch", "storage", "accounts", "blade/url", + "services", "placeholder", "jquery.textOverflow"], function (require, $, fn, rdapi, oauth, jig, - dispatch, storage, accounts, dotCompare, url, + dispatch, storage, accounts, url, services, placeholder) { var store = storage(), - options = url.queryToObject(location.href.split('#')[1] || '') || {}, - existingAccounts = {}, - showNew = options.show === 'new'; + existingAccounts = {}; jig.addFn({ domainType: function (account) { @@ -103,172 +101,31 @@ function (require, $, fn, rdapi, oauth, jig, .append(html) .removeClass('hidden'); } - - //Flash the new items. - if (showNew) { - $(function () { - $("li.newItem").animate({ backgroundColor: '#ffff99' }, 200) - .delay(1000).animate({ backgroundColor: '#fafafa' }, 3000); - }); - } }); } ); $(function () { - //If new items should be shown, refresh the location bar, - //so further reloads of the page do not trigger showNew - if (showNew) { - delete options.show; - location.replace(location.href.split('#')[0] + '#' + url.objectToQuery(options)); - } - - var shortenDom = $('#shortenForm'), - bitlyCheckboxDom = $('#bitlyCheckbox'), - node; - - - //Function placed inside this function to get access to DOM variables. - function getShortenData() { - var data = {}; - - // Clear any error messages from the form. - shortenDom.find('.error').addClass('hidden'); - - $.each(shortenDom[0].elements, function (i, node) { - var trimmed = $(node).val().trim(); - - if (node.getAttribute("placeholder") === trimmed) { - trimmed = ""; - } - - node.value = trimmed; - - if (node.value) { - data[node.name] = node.value; - } - }); - - // Check for error conditions. Must have both API key and login to work. - if (data.login && data.apiKey) { - return data; - } else { - if (data.login && !data.apiKey) { - $('#bitlyApiKeyMissing').removeClass('hidden'); - } else if (data.apiKey && !data.login) { - $('#bitlyLoginMissing').removeClass('hidden'); - } - } - - return null; - } - - function clearShortenData() { - shortenDom.find('[name="login"]').val(''); - shortenDom.find('[name="apiKey"]').val(''); - shortenDom.find('[name="domain"]').val(''); - } - - //Function placed inside this function to get access to DOM variables. - function setShortenData(data) { - $.each(shortenDom[0].elements, function (i, node) { - var value = data[node.getAttribute('name')]; - if (value) { - $(node).val(value); - } - }); - - placeholder(shortenDom[0]); - } - - function showShortenForm() { - bitlyCheckboxDom[0].checked = true; - shortenDom.slideDown('100'); - } - - function hideShortenForm() { - bitlyCheckboxDom[0].checked = false; - shortenDom.slideUp('100', function () { - shortenDom.css({display: 'none'}); - }); - } - - function resetShortenData() { - clearShortenData(); - store.remove('shortenPrefs'); - hideShortenForm(); - } - - // resize wrapper - $(window).bind("load resize", function () { - var h = $(window).height(); - $("#wrapper").css({ "min-height" : (h) }); - }); + $('body') + //Handle button click for services in the settings. + .delegate('#addForm', 'submit', function (evt) { + evt.preventDefault(); - store.get('shortenPrefs', function (shortenPrefs) { - if (shortenPrefs) { - shortenPrefs = JSON.parse(shortenPrefs); - setShortenData(shortenPrefs); - showShortenForm(); - } else { - hideShortenForm(); - } - }); + var node = evt.target, + domain = $('#available').val(), + selectionName; - $('body') - .delegate('#bitlyCheckbox', 'click', function (evt) { - if (bitlyCheckboxDom[0].checked) { - showShortenForm(); - } else { - resetShortenData(); + // If the default option selected which has no domain value is + // used, just return without doing anything. + if (!domain) { + return; } - }) - .delegate('#shortenForm', 'submit', function (evt) { - var data = getShortenData(); - if (data) { - // Confirm that the API key + login name is valid. - $.ajax({ - url: 'http://api.bitly.com/v3/validate', - type: 'GET', - data: { - format: 'json', - login: data.login, - x_login: data.login, - x_apiKey: data.apiKey, - apiKey: data.apiKey - }, - dataType: 'json', - success: function (json) { - if (json.status_code === 200 && json.data.valid) { - store.set('shortenPrefs', JSON.stringify(data)); - } else { - $('#bitlyNotValid').removeClass('hidden'); - store.remove('shortenPrefs'); - } - }, - error: function (xhr, textStatus, errorThrown) { - $('#bitlyNotValid').removeClass('hidden'); - store.remove('shortenPrefs'); - } - }); - } else { - resetShortenData(); - } - evt.preventDefault(); - }) - //Wire up the close button - .delegate('.close', 'click', function (evt) { - window.close(); - }) - //Handle button click for services in the settings. - .delegate('.auth', 'click', function (evt) { - var node = evt.target, - domain = node.getAttribute('data-domain'), - selectionName = services.domains[domain].type; + selectionName = services.domains[domain].type; clearStatus(); + oauth(domain, existingAccounts[domain], function (success) { if (success) { //Make sure to bring the user back to this service if @@ -300,69 +157,5 @@ function (require, $, fn, rdapi, oauth, jig, $(function () { $(".overflow").textOverflow(null, true); }); - - // tabs - // Only show settings if extension can actually handle setting of them. - // Same for advanced. - $('li[data-tab="settings"]').removeClass('hidden'); - $('li[data-tab="advanced"]').removeClass('hidden'); - - $('body') - // Set up tab switching behavior. - .delegate("ul#tabs li", 'click', function (evt) { - var target = $(this), - tabDom = $('#' + target.attr('data-tab')); - - // clear any status that was visible. - clearStatus(); - - // Show tab selected. - target.addClass("selected"); - target.siblings().removeClass("selected"); - - // Show tab contents. - if (tabDom.is(':hidden')) { - tabDom.fadeIn(200); - tabDom.siblings().fadeOut(0); - } - }); - - //Callback handler for JSONP feed response from Google. - window.onFeedLoad = function (x, data) { - var title, link, i, entry; - if (data && data.feed && data.feed.entries) { - for (i = 0; (entry = data.feed.entries[i]); i++) { - if (entry.categories && entry.categories.indexOf('Sharing') !== -1) { - link = entry.link; - title = entry.title; - break; - } - } - } - - if (link) { - $('#newsFooter .headline').removeClass('invisible'); - $('#rssLink').attr('href', link).text(title); - } - }; - - //Fetch the feed. This is low priority, so done at the bottom. - node = document.createElement("script"); - node.charset = "utf-8"; - node.async = true; - node.src = 'https://www.google.com/uds/Gfeeds?v=1.0&callback=onFeedLoad&context=' + - '&output=json&' + - 'q=http%3A%2F%2Fmozillalabs.com%2Fmessaging%2Ffeed%2F'; - $('head')[0].appendChild(node); - - // Make sure this window gets all events, particularly related to storage. - // This can go away if the settings work is done inside the share panel. - // Use a setTimeout because the opener could be reloading, for instance, - // after an account is added. - if (opener && !opener.closed && opener.require) { - setTimeout(function () { - opener.require('dispatch').trackWindow(window); - }, 1000); - } }); }); diff --git a/web/dev/1/settings/style.css b/web/dev/1/settings/style.css index ee3d62e..c297c1e 100644 --- a/web/dev/1/settings/style.css +++ b/web/dev/1/settings/style.css @@ -30,6 +30,7 @@ } body { + margin: 10px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; line-height: 21px; @@ -38,7 +39,6 @@ body { } .hidden, -#tabs li.hidden, .hbox > *.hidden { display: none; } @@ -125,174 +125,6 @@ a:hover { .error { color: #FF0000; } -/* From the uservoice feedback button */ -#feedback { - position: fixed; - top: 40%; - left: 0; - width: 25px; - height: 98px; - padding: 0; - margin: -45px 0 0; - text-indent: -1000px; - background-image: url("https://cdn.uservoice.com/images/widgets/en/feedback_tab_white.png"); - background-position: 2px 50%; - background-color: red; - border-color: #FF0000 #FF0000 #FF0000 -moz-use-text-color; - border-style: outset outset outset none; - border-width: 1px 1px 1px medium; - z-index: 500; -} -#feedback:hover { - text-decoration: none; - background-color: #0066CC; - border-color: #0066CC #0066CC #0066CC -moz-use-text-color; -} - -strong { - font-weight: bold; -} - -#wrapper { - width: 720px; - margin: 0 auto; - overflow: hidden; - position: relative; -} - -#shortenForm label.text { - line-height: 24px; -} - -#shortenForm input { - width: 220px; - height: 24px; - border-color: #aaa; - border-style: solid; - border-width: 1px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15) inset; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 12px; - padding: 2px 5px; -} - -#shortenForm select { - width: 220px; -} - -/* - grid -*/ - -.c1, .c2, .c3 { - display: inline-block; - float: left; - min-height: 1%; - margin: 0 10px; - position: relative; -} - -.c1 { - width: 220px; -} - -.c2 { - width: 460px; -} - -.c3 { - width: 700px; -} - -.row { - float: left; - margin: 10px 0; -} - -.row.about { - margin: 20px 0; -} - -h1 { - font-size: 18px; - font-weight: normal; - color: #00A0FF; -} - -span.micro { - font-size: 12px; - margin: 5px 0 10px 0; - display: block; - color: #666; -} - -a.micro { - margin: 0 10px; -} - -/* - header -*/ - -#header.row { - margin: 36px 0; -} - -#header.row .c3, -#header.row .c2, -#header.row .c1 { - height: 90px; - line-height: 90px; - text-align: right; -} - -#header.row .logo { - background-image: url("i/f1Logo.png"); - background-repeat: no-repeat; - background-position: 0 -9px; -} - -/* - tabs -*/ - -ul#tabs { - display: block; - margin: 10px 10px 0 10px; -} - -ul#tabs li { - display: inline-block; - padding: 7px 20px; - border-width: 1px 1px 0 1px; - border-color: #ccc; - border-style: solid; - float: left; - margin: 0 5px; - background-color: #fff; - cursor: pointer; - -moz-border-radius: 3px 3px 0 0; - -webkit-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; -} - -ul#tabs li.selected { - background-color: #fafafa; - position: relative; - z-index: 2; -} - -/* - config -*/ - -#config { - border-top: 1px solid #ccc; - margin: -1px 10px 0 10px; - position: relative; - z-index: 1; - background-image: -moz-linear-gradient(top, #fafafa 0%, #fff 20px); -} .icon { width: 16px; @@ -329,90 +161,19 @@ ul#tabs li.selected { background-position: center; } -.icon.rss { - background-position: center -271px; - margin: 0; -} - -#done { - padding: 0 10px; -} - -.panel { - width: 100%; - margin: 20px 0; -} - -#config .about { - color: #444; - text-align: center; -} - -#config .username { +.username { color: #aaa; } -#config .new { - color: #ff5959; - font-style: italic; -} - -#config ul { - margin: 10px 0 20px; - border: 1px solid #ccc; - background-color: #fafafa; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - border-radius: 5px; -} - -#config ul li { - width: 100%; - padding: 10px 20px; - border-bottom: 1px solid #ccc; -} - -#config ul li .accountType { - line-height: 24px; -} - -.accountType .multipleSignOut { - float: right; - margin-right: 10px; - color: #AAAAAA; -} - -#config ul li:last-child { - border-bottom: none; - -moz-border-radius: 0 0 5px 5px; - -webkit-border-radius: 0 0 5px 5px; - border-radius: 0 0 5px 5px; -} - -#settings .key { - margin-right: 10px; - padding: 0 3px 0 5px; - -moz-border-radius: 4px; - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.75), 0 1px 0 rgba(0, 0, 0, 0.3); - background: -moz-linear-gradient(#f8f8f8, #dddee0) repeat scroll 0 0 #ecedef; - color: #434343; - font-weight: bold; - text-align: center; - text-shadow: 0 1px 1px white; - -moz-user-select: none; -} - -#newsFooter { - padding: 0 10px; - margin: 10px 0; - position: absolute; - bottom: 0; - width: 720px; +#addForm { + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #AAAAAA; + text-align: right; } -#newsFooter .privacy { - margin: 0 0 0 10px; - color: #444; +#manage li { + margin-bottom: 10px; } /* diff --git a/web/dev/1/share/panel/index.html b/web/dev/1/share/panel/index.html index 801aeb3..bbba330 100644 --- a/web/dev/1/share/panel/index.html +++ b/web/dev/1/share/panel/index.html @@ -1,37 +1,41 @@ - F1: Popup + Share - + + -