~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/canonical/launchpad/doc/webapp-publication.txt

  • Committer: Danilo Segan
  • Date: 2011-04-22 14:02:29 UTC
  • mto: This revision was merged to the branch mainline in revision 12910.
  • Revision ID: danilo@canonical.com-20110422140229-zhq4d4c2k8jpglhf
Ignore hidden files when building combined JS file.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
Launchpad Publication
2
 
=====================
 
1
= Launchpad Publication =
3
2
 
4
3
Launchpad uses the generic Zope3 publisher. It registers several
5
4
factories that are responsible for instantiating the appropriate
7
6
zope.publisher.IPublication for the request.
8
7
 
9
8
 
10
 
Virtual host configurations
11
 
---------------------------
 
9
== Virtual host configurations ==
12
10
 
13
11
The configuration defines a number of domains, one for the main
14
12
Launchpad site and one for the sites of the various applications.
15
13
 
16
 
    >>> from lp.services.config import config
 
14
    >>> from canonical.config import config
17
15
    >>> config.vhost.mainsite.hostname
18
16
    'launchpad.dev'
19
17
    >>> config.vhost.blueprints.hostname
27
25
 
28
26
These are parsed into webapp.vhost.allvhosts.
29
27
 
30
 
    >>> from lp.services.webapp.vhosts import allvhosts
 
28
    >>> from canonical.launchpad.webapp.vhosts import allvhosts
31
29
    >>> allvhosts.use_https
32
30
    False
33
31
    >>> for confname, vhost in sorted(allvhosts.configs.items()):
83
81
    rooturl: http://ubuntu-openid.launchpad.dev/
84
82
    althosts:
85
83
    ----
 
84
    vostok @ vostok.dev
 
85
    rooturl: http://vostok.dev/
 
86
    althosts:
 
87
    ----
86
88
    xmlrpc @ xmlrpc.launchpad.dev
87
89
    rooturl: http://launchpad.dev/
88
90
    althosts:
111
113
    testopenid.dev
112
114
    translations.launchpad.dev
113
115
    ubuntu-openid.launchpad.dev
 
116
    vostok.dev
114
117
    xmlrpc-private.launchpad.dev
115
118
    xmlrpc.launchpad.dev
116
119
 
117
120
 
118
 
VirtualHostRequestPublicationFactory
119
 
------------------------------------
 
121
== VirtualHostRequestPublicationFactory ==
120
122
 
121
123
A number of VirtualHostRequestPublicationFactories are registered with
122
124
Zope to handle requests for a particular vhost, port, and set of HTTP
123
125
methods.
124
126
 
125
127
    >>> from cStringIO import StringIO
126
 
    >>> from lp.services.webapp.publication import (
 
128
    >>> from canonical.launchpad.webapp.publication import (
127
129
    ...     LaunchpadBrowserPublication)
128
 
    >>> from lp.services.webapp.servers import (
 
130
    >>> from canonical.launchpad.webapp.servers import (
129
131
    ...     LaunchpadBrowserRequest, VirtualHostRequestPublicationFactory)
130
132
    >>> from zope.app.publication.interfaces import IRequestPublicationFactory
131
 
    >>> from lp.services.webapp.testing import verifyObject
 
133
    >>> from canonical.launchpad.webapp.testing import verifyObject
132
134
 
133
135
Those factories provide the IRequestPublicationFactory interface.
134
136
 
187
189
 
188
190
    >>> requestfactory, publicationfactory = factory()
189
191
    >>> publicationfactory
190
 
    <class '...LaunchpadBrowserPublication'>
 
192
    <class 'canonical.launchpad.webapp.publication.LaunchpadBrowserPublication'>
191
193
 
192
194
If the request comes in on one of the virtual hosts, the request
193
195
factory is wrapped in an ApplicationServerSettingRequestFactory that
195
197
host configured settings.
196
198
 
197
199
    >>> type(requestfactory)
198
 
    <class '...ApplicationServerSettingRequestFactory'>
 
200
    <class 'canonical.launchpad.webapp.servers.ApplicationServerSettingRequestFactory'>
199
201
    >>> request = requestfactory(StringIO(''), environment)
200
202
    >>> type(request)
201
 
    <class 'lp.services.webapp.servers.LaunchpadBrowserRequest'>
 
203
    <class 'canonical.launchpad.webapp.servers.LaunchpadBrowserRequest'>
202
204
    >>> request.getApplicationURL()
203
205
    'http://launchpad.dev'
204
206
 
211
213
    True
212
214
    >>> requestfactory, publicationfactory = default_handling_factory()
213
215
    >>> requestfactory
214
 
    <class 'lp.services.webapp.servers.LaunchpadBrowserRequest'>
 
216
    <class 'canonical.launchpad.webapp.servers.LaunchpadBrowserRequest'>
215
217
 
216
218
    >>> environment = {'REQUEST_METHOD': 'GET'}
217
219
    >>> default_handling_factory.canHandle(environment)
218
220
    True
219
221
    >>> requestfactory, publicationfactory = default_handling_factory()
220
222
    >>> requestfactory
221
 
    <class 'lp.services.webapp.servers.LaunchpadBrowserRequest'>
 
223
    <class 'canonical.launchpad.webapp.servers.LaunchpadBrowserRequest'>
222
224
 
223
225
A request publication factory will not handle requests unless they're
224
226
directed to one of its registered host names.
299
301
 
300
302
    >>> requestfactory, publicationfactory = factory()
301
303
    >>> publicationfactory
302
 
    <lp.services.webapp.servers.ProtocolErrorPublicationFactory ...>
 
304
    <canonical.launchpad.webapp.servers.ProtocolErrorPublicationFactory ...>
303
305
 
304
306
    >>> factory = VirtualHostRequestPublicationFactory(
305
307
    ...     'mainsite', LaunchpadBrowserRequest, LaunchpadBrowserPublication,
310
312
    True
311
313
    >>> requestfactory, publicationfactory = factory()
312
314
    >>> publicationfactory
313
 
    <lp.services.webapp.servers.ProtocolErrorPublicationFactory ...>
 
315
    <canonical.launchpad.webapp.servers.ProtocolErrorPublicationFactory ...>
314
316
 
315
317
    >>> environment['REQUEST_METHOD'] = 'DELETE'
316
318
    >>> factory.canHandle(environment)
317
319
    True
318
320
    >>> requestfactory, publicationfactory = factory()
319
321
    >>> publicationfactory
320
 
    <class '...LaunchpadBrowserPublication'>
321
 
 
322
 
 
323
 
Zope Publisher integration
324
 
--------------------------
 
322
    <class 'canonical.launchpad.webapp.publication.LaunchpadBrowserPublication'>
 
323
 
 
324
 
 
325
== Zope Publisher integration ==
325
326
 
326
327
A factory is registered for each of our available virtual host. This
327
328
is done by the register_launchpad_request_publication_factories
330
331
(We need to call it here once again, because the test layer clears out
331
332
the registered factories.)
332
333
 
333
 
    >>> from lp.services.webapp.servers import (
 
334
    >>> from canonical.launchpad.webapp.servers import (
334
335
    ...     register_launchpad_request_publication_factories)
335
336
    >>> register_launchpad_request_publication_factories()
336
337
 
416
417
      Allow: POST
417
418
 
418
419
    >>> print_request_and_publication(
419
 
    ...     'xmlrpc.launchpad.dev', method='POST',
420
 
    ...     mime_type='application/xml')
 
420
    ...     'xmlrpc.launchpad.dev', method='POST', mime_type='application/xml')
421
421
    ProtocolErrorRequest
422
422
    ProtocolErrorPublication: status=415
423
423
 
483
483
    >>> login(ANONYMOUS)
484
484
 
485
485
 
486
 
ILaunchpadBrowserApplicationRequest
487
 
-----------------------------------
 
486
== ILaunchpadBrowserApplicationRequest ==
488
487
 
489
488
All Launchpad requests provides the ILaunchpadBrowserApplicationRequest
490
489
interface. That interface is an extension of the zope standard
491
490
IBrowserApplicationRequest.
492
491
 
493
 
    >>> from lp.services.webapp.interfaces import (
 
492
    >>> from canonical.launchpad.webapp.interfaces import (
494
493
    ...     ILaunchpadBrowserApplicationRequest)
495
494
 
496
495
    >>> request, publication = get_request_and_publication()
498
497
    True
499
498
 
500
499
 
501
 
Handling form data using IBrowserFormNG
502
 
---------------------------------------
 
500
== Handling form data using IBrowserFormNG ==
503
501
 
504
502
Submitted form data is available in the form_ng request attribute. This
505
503
is an object providing the IBrowserFormNG interface which offers two
506
504
methods to obtain form data. (Form data is also available through the
507
505
regular Zope3 form attribute using the dictionary interface.)
508
506
 
509
 
    >>> from lp.services.webapp.interfaces import IBrowserFormNG
 
507
    >>> from canonical.launchpad.webapp.interfaces import IBrowserFormNG
510
508
    >>> verifyObject(IBrowserFormNG, request.form_ng)
511
509
    True
512
510
 
513
511
You can check the presence of an uploaded field using the regular
514
512
python 'in' operator.
515
513
 
516
 
    >>> from lp.services.webapp.servers import (
 
514
    >>> from canonical.launchpad.webapp.servers import (
517
515
    ...     LaunchpadBrowserRequest)
518
516
    >>> from urllib import urlencode
519
517
    >>> environment = {'QUERY_STRING': urlencode({
584
582
    items_field
585
583
 
586
584
 
587
 
Page ID
588
 
-------
 
585
== Page ID ==
589
586
 
590
587
Our publication implementation sets a WSGI variable 'launchpad.pageid'.
591
588
This is an identifier of the form ContextName:ViewName.
592
589
 
593
 
    >>> from lp.services.webapp.interfaces import (
 
590
    >>> from canonical.launchpad.webapp.interfaces import (
594
591
    ...     IPlacelessAuthUtility)
595
592
    >>> auth_utility = getUtility(IPlacelessAuthUtility)
596
593
    >>> request, publication = get_request_and_publication()
657
654
    ''
658
655
 
659
656
 
660
 
Tick counts
661
 
-----------
 
657
== Tick counts ==
662
658
 
663
659
Similarly to our page IDs, our publication implementation will store the
664
660
tick counts for the traversal and object publication processes in WSGI
704
700
If an exception is raised during traversal or object publication, we'll
705
701
store the ticks up to the point in which the exception is raised.  This
706
702
is done inside the handleException() hook.  (The hook also sets and resets
707
 
the request timer from lp.services.webapp.adapter, so you'll notice
 
703
the request timer from canonical.launchpad.webapp.adapter, so you'll notice
708
704
some calls to prepare that code to what handleException expects.)
709
705
 
710
706
If the exception is raised before we even start the traversal, then
711
707
there's nothing to store.
712
708
 
713
 
    >>> from lp.services.webapp.adapter import (
 
709
    >>> from canonical.launchpad.webapp.adapter import (
714
710
    ...     clear_request_started, set_request_started)
715
711
    >>> request, publication = get_request_and_publication()
716
712
    >>> request.setPrincipal(auth_utility.unauthenticatedPrincipal())
836
832
    >>> login(ANONYMOUS)
837
833
 
838
834
 
839
 
Transaction Logging
840
 
-------------------
 
835
== Transaction Logging ==
841
836
 
842
837
The publication implementation is responsible for putting the name
843
838
of the logged in user in the transaction. (The afterCall() hook is
877
872
     / 16
878
873
 
879
874
 
880
 
Read-Only Requests
881
 
------------------
 
875
== Read-Only Requests ==
882
876
 
883
877
Our publication implementation make sure that requests supposed to be
884
878
read-only (GET and HEAD) don't change anything in the database.
890
884
some string in its finishReadOnlyRequest().
891
885
 
892
886
    >>> class MyPublication(LaunchpadBrowserPublication):
893
 
    ...     def finishReadOnlyRequest(self, request, ob, txn):
 
887
    ...     def finishReadOnlyRequest(self, txn):
894
888
    ...         print "booo!"
895
889
 
896
890
    >>> publication = MyPublication(None)
900
894
In the default implementation, the following database modification will
901
895
be automatically reverted in a GET request.
902
896
 
903
 
    >>> from lp.services.identity.model.emailaddress import EmailAddress
904
 
    >>> from lp.services.database.lpstorm import IMasterStore
 
897
    >>> from canonical.launchpad.database.emailaddress import EmailAddress
 
898
    >>> from canonical.launchpad.ftests import syncUpdate
 
899
    >>> from canonical.launchpad.interfaces.lpstorm import IMasterStore
905
900
    >>> from lp.registry.model.person import Person
906
901
    >>> login('foo.bar@canonical.com')
907
902
    >>> txn = transaction.begin()
914
909
    >>> print foo_bar.homepage_content
915
910
    None
916
911
    >>> foo_bar.homepage_content = 'Montreal'
 
912
    >>> syncUpdate(foo_bar)
917
913
 
918
914
    >>> request, publication = get_request_and_publication(method='GET')
919
915
 
932
928
 
933
929
    >>> txn = transaction.begin()
934
930
    >>> get_foo_bar_person().homepage_content = 'Darwin'
 
931
    >>> syncUpdate(foo_bar)
935
932
 
936
933
    >>> request, publication = get_request_and_publication(method='POST')
937
934
 
946
943
    Darwin
947
944
 
948
945
 
949
 
GET requests on api.launchpad.net
950
 
.................................
 
946
=== GET requests on api.launchpad.net ===
951
947
 
952
948
In the case of WebServicePublication, though, we have to commit the
953
949
transaction after GET requests in order to persist new entries added to
954
950
the OAuthNonce table.
955
951
 
956
952
    >>> import time
957
 
    >>> from lp.services.oauth.interfaces import IOAuthConsumerSet
958
 
    >>> from lp.services.webapp.interfaces import OAuthPermission
 
953
    >>> from canonical.launchpad.interfaces.oauth import IOAuthConsumerSet
 
954
    >>> from canonical.launchpad.webapp.interfaces import OAuthPermission
959
955
    >>> from lp.registry.interfaces.person import IPersonSet
960
956
 
961
957
    >>> from zope.component import getUtility
962
 
    >>> from lp.services.webapp.dbpolicy import MasterDatabasePolicy
963
 
    >>> from lp.services.webapp.interfaces import IStoreSelector
 
958
    >>> from canonical.launchpad.webapp.dbpolicy import MasterDatabasePolicy
 
959
    >>> from canonical.launchpad.webapp.interfaces import IStoreSelector
964
960
    >>> getUtility(IStoreSelector).push(MasterDatabasePolicy())
965
961
 
966
962
    >>> salgado = getUtility(IPersonSet).getByName('salgado')
996
992
    ...     'api.launchpad.dev', 'GET',
997
993
    ...     extra_environment=dict(QUERY_STRING=urlencode(form)))
998
994
    >>> test_request.processInputs()
999
 
    >>> publication.getPrincipal(test_request)
 
995
    >>> print publication.getPrincipal(test_request).title
1000
996
    Traceback (most recent call last):
1001
997
    ...
1002
 
    NonceAlreadyUsed: This nonce has been used already.
1003
 
 
1004
 
 
1005
 
Doomed transactions are aborted
1006
 
-------------------------------
 
998
    Unauthorized: Invalid nonce/timestamp: This nonce has been used already.
 
999
 
 
1000
 
 
1001
== Doomed transactions are aborted ==
1007
1002
 
1008
1003
Doomed transactions are aborted.
1009
1004
 
1037
1032
    >>> del txn.abort # Clean up test fixture.
1038
1033
 
1039
1034
 
1040
 
Requests on Python C Methods succeed
1041
 
------------------------------------
 
1035
== Requests on Python C Methods succeed ==
1042
1036
 
1043
1037
Rarely but occasionally, it is possible to traverse to a Python C method.
1044
1038
For instance, an XMLRPC proxy might allow a traversal to __repr__.
1054
1048
    '{}'
1055
1049
 
1056
1050
 
1057
 
HEAD requests have empty body
1058
 
-----------------------------
 
1051
== HEAD requests have empty body ==
1059
1052
 
1060
1053
The publication implementation also makes sure that no body is
1061
1054
returned as part of HEAD requests. (Again this is handled by the
1092
1085
    Some boring content.
1093
1086
 
1094
1087
 
1095
 
Authentication of requests
1096
 
--------------------------
 
1088
== Authentication of requests ==
1097
1089
 
1098
1090
In LaunchpadBrowserPublication, authentication happens in the
1099
1091
beforeTraversal() hook. Our publication will set the principal to
1108
1100
    ...         return marker
1109
1101
 
1110
1102
    >>> publication = MyPublication(None)
1111
 
    >>> from lp.services.webapp.servers import LaunchpadTestRequest
 
1103
    >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
1112
1104
    >>> request = LaunchpadTestRequest()
1113
1105
 
1114
1106
    # We need to close the previous interaction.
1187
1179
    >>> import pytz
1188
1180
    >>> now = datetime.now(pytz.timezone('UTC'))
1189
1181
    >>> access_token.date_expires = now - timedelta(days=1)
 
1182
    >>> syncUpdate(access_token)
1190
1183
    >>> form2 = form.copy()
1191
1184
    >>> form2['oauth_nonce'] = '1764572616e48616d6d65724c61686'
1192
1185
    >>> test_request = LaunchpadTestRequest(form=form2)
1193
 
    >>> publication.getPrincipal(test_request)
 
1186
    >>> print publication.getPrincipal(test_request).title
1194
1187
    Traceback (most recent call last):
1195
1188
    ...
1196
 
    TokenException: Expired token...
1197
 
 
 
1189
    Unauthorized: Expired token...
1198
1190
    >>> access_token.date_expires = now + timedelta(days=1)
 
1191
    >>> syncUpdate(access_token)
1199
1192
 
1200
1193
    >>> form2 = form.copy()
1201
1194
    >>> form2['oauth_token'] += 'z'
1202
1195
    >>> form2['oauth_nonce'] = '4572616e48616d6d65724c61686176'
1203
1196
    >>> test_request = LaunchpadTestRequest(form=form2)
1204
 
    >>> publication.getPrincipal(test_request)
 
1197
    >>> print publication.getPrincipal(test_request).title
1205
1198
    Traceback (most recent call last):
1206
1199
    ...
1207
 
    TokenException: Unknown access token...
 
1200
    Unauthorized: Unknown access token...
1208
1201
 
1209
1202
The consumer must be registered as well, and the signature must be
1210
1203
correct.
1212
1205
    >>> form2 = form.copy()
1213
1206
    >>> form2['oauth_consumer_key'] += 'z'
1214
1207
    >>> test_request = LaunchpadTestRequest(form=form2)
1215
 
    >>> publication.getPrincipal(test_request)
 
1208
    >>> print publication.getPrincipal(test_request).title
1216
1209
    Traceback (most recent call last):
1217
1210
    ...
1218
1211
    Unauthorized: Unknown consumer (foobar123451432z).
1221
1214
    >>> form2['oauth_signature'] += 'z'
1222
1215
    >>> form2['oauth_nonce'] = '2616e48616d6d65724c61686176457'
1223
1216
    >>> test_request = LaunchpadTestRequest(form=form2)
1224
 
    >>> publication.getPrincipal(test_request)
 
1217
    >>> print publication.getPrincipal(test_request).title
1225
1218
    Traceback (most recent call last):
1226
1219
    ...
1227
 
    TokenException: Invalid signature.
 
1220
    Unauthorized: Invalid signature.
1228
1221
 
1229
1222
The nonce specified in the request must not have been used before in a request
1230
1223
with this same token and timestamp.
1236
1229
    Guilherme Salgado
1237
1230
 
1238
1231
    >>> test_request = LaunchpadTestRequest(form=form2)
1239
 
    >>> publication.getPrincipal(test_request)
 
1232
    >>> print publication.getPrincipal(test_request).title
1240
1233
    Traceback (most recent call last):
1241
1234
    ...
1242
 
    NonceAlreadyUsed: This nonce has been used already.
 
1235
    Unauthorized: Invalid nonce/timestamp: This nonce has been used already.
1243
1236
 
1244
1237
The timestamp must not be older than TIMESTAMP_ACCEPTANCE_WINDOW from the most
1245
1238
recent request for this token.
1246
1239
 
1247
 
    >>> from lp.services.oauth.model import (
 
1240
    >>> from canonical.launchpad.database.oauth import (
1248
1241
    ...     TIMESTAMP_ACCEPTANCE_WINDOW)
1249
1242
    >>> form2 = form.copy()
1250
1243
    >>> form2['oauth_timestamp'] -= (TIMESTAMP_ACCEPTANCE_WINDOW-5)
1254
1247
 
1255
1248
    >>> form2['oauth_timestamp'] -= 10
1256
1249
    >>> test_request = LaunchpadTestRequest(form=form2)
1257
 
    >>> publication.getPrincipal(test_request)
 
1250
    >>> print publication.getPrincipal(test_request).title
1258
1251
    Traceback (most recent call last):
1259
1252
    ...
1260
 
    TimestampOrderingError: Timestamp too old compared ...
 
1253
    Unauthorized: Invalid nonce/timestamp: Timestamp too old compared ...
1261
1254
 
1262
1255
Last but not least, the timestamp must not be too far in the future, as
1263
1256
defined by TIMESTAMP_SKEW_WINDOW.
1264
1257
 
1265
 
    >>> from lp.services.oauth.model import (
 
1258
    >>> from canonical.launchpad.database.oauth import (
1266
1259
    ...     TIMESTAMP_SKEW_WINDOW)
1267
1260
    >>> form2 = form.copy()
1268
1261
    >>> form2['oauth_timestamp'] += (TIMESTAMP_SKEW_WINDOW+10)
1269
1262
    >>> test_request = LaunchpadTestRequest(form=form2)
1270
 
    >>> publication.getPrincipal(test_request)
 
1263
    >>> print publication.getPrincipal(test_request).title
1271
1264
    Traceback (most recent call last):
1272
1265
    ...
1273
 
    ClockSkew: Timestamp ... from bad system clock
 
1266
    Unauthorized: Invalid nonce/timestamp: Timestamp ... from bad system clock
1274
1267
 
1275
1268
Close the bogus request that was started by the call to
1276
1269
beforeTraversal, in order to ensure we leave our state sane.