traclightningのリポジトリ
Revision | 9be46ec8635a4d0d2470626ab074efb57fce7ccb (tree) |
---|---|
Time | 2011-05-30 19:44:56 |
Author | kanu_orz |
Commiter | kanu_orz |
update xmlrpcplugin to 1.1.2
@@ -0,0 +1,29 @@ | ||
1 | +K 25 | |
2 | +svn:wc:ra_dav:version-url | |
3 | +V 37 | |
4 | +/svn/!svn/ver/9970/xmlrpcplugin/trunk | |
5 | +END | |
6 | +README.wiki | |
7 | +K 25 | |
8 | +svn:wc:ra_dav:version-url | |
9 | +V 49 | |
10 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/README.wiki | |
11 | +END | |
12 | +setup.py | |
13 | +K 25 | |
14 | +svn:wc:ra_dav:version-url | |
15 | +V 46 | |
16 | +/svn/!svn/ver/9912/xmlrpcplugin/trunk/setup.py | |
17 | +END | |
18 | +MANIFEST.in | |
19 | +K 25 | |
20 | +svn:wc:ra_dav:version-url | |
21 | +V 49 | |
22 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/MANIFEST.in | |
23 | +END | |
24 | +setup.cfg | |
25 | +K 25 | |
26 | +svn:wc:ra_dav:version-url | |
27 | +V 47 | |
28 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/setup.cfg | |
29 | +END |
@@ -0,0 +1,8 @@ | ||
1 | +K 10 | |
2 | +svn:ignore | |
3 | +V 22 | |
4 | +build | |
5 | +dist | |
6 | +rpctestenv | |
7 | + | |
8 | +END |
@@ -0,0 +1,167 @@ | ||
1 | +10 | |
2 | + | |
3 | +dir | |
4 | +10247 | |
5 | +http://trac-hacks.org/svn/xmlrpcplugin/trunk | |
6 | +http://trac-hacks.org/svn | |
7 | + | |
8 | + | |
9 | + | |
10 | +2011-03-18T10:32:52.709335Z | |
11 | +9970 | |
12 | +osimons | |
13 | +has-props | |
14 | + | |
15 | + | |
16 | + | |
17 | + | |
18 | + | |
19 | + | |
20 | + | |
21 | + | |
22 | + | |
23 | + | |
24 | + | |
25 | + | |
26 | + | |
27 | +7322e99d-02ea-0310-aa39-e9a107903beb | |
28 | + | |
29 | +README.wiki | |
30 | +file | |
31 | + | |
32 | + | |
33 | + | |
34 | + | |
35 | +2010-05-02T23:49:30.648005Z | |
36 | +7349e9210f1c5664e6aec9d7c1236379 | |
37 | +2010-05-02T23:49:30.648005Z | |
38 | +7916 | |
39 | +osimons | |
40 | + | |
41 | + | |
42 | + | |
43 | + | |
44 | + | |
45 | + | |
46 | + | |
47 | + | |
48 | + | |
49 | + | |
50 | + | |
51 | + | |
52 | + | |
53 | + | |
54 | + | |
55 | + | |
56 | + | |
57 | + | |
58 | + | |
59 | + | |
60 | + | |
61 | +3164 | |
62 | + | |
63 | +setup.py | |
64 | +file | |
65 | + | |
66 | + | |
67 | + | |
68 | + | |
69 | +2011-03-02T03:40:36.325896Z | |
70 | +ea55ef999127c930d13f0a9a5595bae5 | |
71 | +2011-03-02T03:40:36.325896Z | |
72 | +9912 | |
73 | +osimons | |
74 | + | |
75 | + | |
76 | + | |
77 | + | |
78 | + | |
79 | + | |
80 | + | |
81 | + | |
82 | + | |
83 | + | |
84 | + | |
85 | + | |
86 | + | |
87 | + | |
88 | + | |
89 | + | |
90 | + | |
91 | + | |
92 | + | |
93 | + | |
94 | + | |
95 | +976 | |
96 | + | |
97 | +MANIFEST.in | |
98 | +file | |
99 | + | |
100 | + | |
101 | + | |
102 | + | |
103 | +2010-05-02T23:49:30.648005Z | |
104 | +0d763c82345f2b417d66cea03ca6df94 | |
105 | +2010-05-02T23:49:30.648005Z | |
106 | +7916 | |
107 | +osimons | |
108 | + | |
109 | + | |
110 | + | |
111 | + | |
112 | + | |
113 | + | |
114 | + | |
115 | + | |
116 | + | |
117 | + | |
118 | + | |
119 | + | |
120 | + | |
121 | + | |
122 | + | |
123 | + | |
124 | + | |
125 | + | |
126 | + | |
127 | + | |
128 | + | |
129 | +128 | |
130 | + | |
131 | +tracrpc | |
132 | +dir | |
133 | + | |
134 | +setup.cfg | |
135 | +file | |
136 | + | |
137 | + | |
138 | + | |
139 | + | |
140 | +2010-05-02T23:49:30.648005Z | |
141 | +4754f6eaa317fc35b7e24989a2b6307c | |
142 | +2010-05-02T23:49:30.648005Z | |
143 | +7916 | |
144 | +osimons | |
145 | + | |
146 | + | |
147 | + | |
148 | + | |
149 | + | |
150 | + | |
151 | + | |
152 | + | |
153 | + | |
154 | + | |
155 | + | |
156 | + | |
157 | + | |
158 | + | |
159 | + | |
160 | + | |
161 | + | |
162 | + | |
163 | + | |
164 | + | |
165 | + | |
166 | +48 | |
167 | + |
@@ -0,0 +1,5 @@ | ||
1 | +include README.wiki | |
2 | +include setup.cfg | |
3 | +include tracrpc/htdocs/*.css | |
4 | +include tracrpc/htdocs/*.js | |
5 | +include tracrpc/templates/*.html |
@@ -0,0 +1,92 @@ | ||
1 | += Trac RPC plugin = | |
2 | + | |
3 | +Remote Procedure Call interface for Trac. | |
4 | + | |
5 | +Protocols: | |
6 | + * XML-RPC | |
7 | + * JSON-RPC | |
8 | + | |
9 | +API support: | |
10 | + * search | |
11 | + * system | |
12 | + * ticket | |
13 | + * ticket.component | |
14 | + * ticket.milestone | |
15 | + * ticket.priority | |
16 | + * ticket.resolution | |
17 | + * ticket.severity | |
18 | + * ticket.status | |
19 | + * ticket.type | |
20 | + * ticket.version | |
21 | + * wiki | |
22 | + | |
23 | +== Installing and Using == | |
24 | + | |
25 | +See http://trac-hacks.org/wiki/XmlRpcPlugin for details on how to install, how | |
26 | +get help, and how to report issues. | |
27 | + | |
28 | +== API Documentation == | |
29 | + | |
30 | +The API documentation is available at `<project_url>/rpc` for projects that | |
31 | +have the plugin installed and enabled. It can be accessed by all users that | |
32 | +have been granted `XML_RPC` permission. | |
33 | + | |
34 | +== Development == | |
35 | + | |
36 | +The Trac RPC plugin uses pluggable interfaces to do all its work. That means it | |
37 | +is easy to extend, and currently supports: | |
38 | + * protocols; add a new protocol in addition to the builtin ones and read input | |
39 | + and answer request in whatever form and format needed. | |
40 | + * methods; adding new methods available for remote procedure calls that will | |
41 | + work for any enabled protocol. | |
42 | + | |
43 | +See source for documentation. The source code can be obtained from: | |
44 | + | |
45 | +http://trac-hacks.org/svn/xmlrpcplugin/ | |
46 | + | |
47 | +For work on the plugin itself (for submitting patches and more), please verify | |
48 | +patches by running unittests (requires Trac source code on path): | |
49 | +{{{ | |
50 | +python setup.py test | |
51 | +}}} | |
52 | + | |
53 | +== Thanks == | |
54 | + | |
55 | +Thanks to all those that use the plugin, and contribute with error reports, | |
56 | +and patches for bugs and enhancements. Special thanks to: | |
57 | + * Matt Good | |
58 | + * Steffen Pingel | |
59 | + * Olemis Lang | |
60 | + | |
61 | +== License == | |
62 | + | |
63 | +{{{ | |
64 | +Copyright (c) 2005-2008, Alec Thomas (alec@swapoff.org) | |
65 | +Copyright (c) 2009, CodeResort.com/BV Network AS (simon-code@bvnetwork.no) | |
66 | + | |
67 | +All rights reserved. | |
68 | + | |
69 | +Redistribution and use in source and binary forms, with or without | |
70 | +modification, are permitted provided that the following conditions are met: | |
71 | + | |
72 | + 1. Redistributions of source code must retain the above copyright notice, | |
73 | + this list of conditions and the following disclaimer. | |
74 | + 2. Redistributions in binary form must reproduce the above copyright notice, | |
75 | + this list of conditions and the following disclaimer in the documentation | |
76 | + and/or other materials provided with the distribution. | |
77 | + 3. Neither the name of the copyright holder(s) nor the names of its | |
78 | + contributors may be used to endorse or promote products derived from this | |
79 | + software without specific prior written permission. | |
80 | + | |
81 | +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
82 | +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
83 | +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
84 | +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
85 | +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
86 | +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
87 | +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
88 | +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
89 | +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
90 | +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | |
91 | +THE POSSIBILITY OF SUCH DAMAGE. | |
92 | +}}} |
@@ -0,0 +1,3 @@ | ||
1 | +[egg_info] | |
2 | +tag_build = | |
3 | +tag_svn_revision = true |
@@ -0,0 +1,40 @@ | ||
1 | +#!/usr/bin/env python | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +import sys | |
10 | + | |
11 | +from setuptools import setup, find_packages | |
12 | + | |
13 | +try : | |
14 | + import crypt | |
15 | +except ImportError : | |
16 | + test_deps = ['twill', 'fcrypt'] | |
17 | +else : | |
18 | + test_deps = ['twill'] | |
19 | + | |
20 | +setup( | |
21 | + name='TracXMLRPC', | |
22 | + version='1.1.2', | |
23 | + license='BSD', | |
24 | + author='Alec Thomas', | |
25 | + author_email='alec@swapoff.org', | |
26 | + maintainer='Odd Simon Simonsen', | |
27 | + maintainer_email='simon-code@bvnetwork.no', | |
28 | + url='http://trac-hacks.org/wiki/XmlRpcPlugin', | |
29 | + description='RPC interface to Trac', | |
30 | + zip_safe=True, | |
31 | + test_suite = 'tracrpc.tests.test_suite', | |
32 | + tests_require = test_deps, | |
33 | + packages=find_packages(exclude=['*.tests']), | |
34 | + package_data={ | |
35 | + 'tracrpc': ['templates/*.html', 'htdocs/*.js', 'htdocs/*.css'] | |
36 | + }, | |
37 | + entry_points={ | |
38 | + 'trac.plugins': 'TracXMLRPC = tracrpc' | |
39 | + }, | |
40 | + ) |
@@ -19,7 +19,7 @@ | ||
19 | 19 | |
20 | 20 | setup( |
21 | 21 | name='TracXMLRPC', |
22 | - version='1.1.0', | |
22 | + version='1.1.2', | |
23 | 23 | license='BSD', |
24 | 24 | author='Alec Thomas', |
25 | 25 | author_email='alec@swapoff.org', |
@@ -0,0 +1,59 @@ | ||
1 | +K 25 | |
2 | +svn:wc:ra_dav:version-url | |
3 | +V 45 | |
4 | +/svn/!svn/ver/9970/xmlrpcplugin/trunk/tracrpc | |
5 | +END | |
6 | +api.py | |
7 | +K 25 | |
8 | +svn:wc:ra_dav:version-url | |
9 | +V 52 | |
10 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/tracrpc/api.py | |
11 | +END | |
12 | +web_ui.py | |
13 | +K 25 | |
14 | +svn:wc:ra_dav:version-url | |
15 | +V 55 | |
16 | +/svn/!svn/ver/9970/xmlrpcplugin/trunk/tracrpc/web_ui.py | |
17 | +END | |
18 | +json_rpc.py | |
19 | +K 25 | |
20 | +svn:wc:ra_dav:version-url | |
21 | +V 57 | |
22 | +/svn/!svn/ver/7957/xmlrpcplugin/trunk/tracrpc/json_rpc.py | |
23 | +END | |
24 | +util.py | |
25 | +K 25 | |
26 | +svn:wc:ra_dav:version-url | |
27 | +V 53 | |
28 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/tracrpc/util.py | |
29 | +END | |
30 | +__init__.py | |
31 | +K 25 | |
32 | +svn:wc:ra_dav:version-url | |
33 | +V 57 | |
34 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/tracrpc/__init__.py | |
35 | +END | |
36 | +ticket.py | |
37 | +K 25 | |
38 | +svn:wc:ra_dav:version-url | |
39 | +V 55 | |
40 | +/svn/!svn/ver/9912/xmlrpcplugin/trunk/tracrpc/ticket.py | |
41 | +END | |
42 | +wiki.py | |
43 | +K 25 | |
44 | +svn:wc:ra_dav:version-url | |
45 | +V 53 | |
46 | +/svn/!svn/ver/9818/xmlrpcplugin/trunk/tracrpc/wiki.py | |
47 | +END | |
48 | +xml_rpc.py | |
49 | +K 25 | |
50 | +svn:wc:ra_dav:version-url | |
51 | +V 56 | |
52 | +/svn/!svn/ver/9360/xmlrpcplugin/trunk/tracrpc/xml_rpc.py | |
53 | +END | |
54 | +search.py | |
55 | +K 25 | |
56 | +svn:wc:ra_dav:version-url | |
57 | +V 55 | |
58 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/tracrpc/search.py | |
59 | +END |
@@ -0,0 +1,343 @@ | ||
1 | +10 | |
2 | + | |
3 | +dir | |
4 | +10247 | |
5 | +http://trac-hacks.org/svn/xmlrpcplugin/trunk/tracrpc | |
6 | +http://trac-hacks.org/svn | |
7 | + | |
8 | + | |
9 | + | |
10 | +2011-03-18T10:32:52.709335Z | |
11 | +9970 | |
12 | +osimons | |
13 | + | |
14 | + | |
15 | + | |
16 | + | |
17 | + | |
18 | + | |
19 | + | |
20 | + | |
21 | + | |
22 | + | |
23 | + | |
24 | + | |
25 | + | |
26 | + | |
27 | +7322e99d-02ea-0310-aa39-e9a107903beb | |
28 | + | |
29 | +api.py | |
30 | +file | |
31 | + | |
32 | + | |
33 | + | |
34 | + | |
35 | +2010-05-02T23:49:30.648005Z | |
36 | +da02cc0ce0793a6350390f079840f2df | |
37 | +2010-05-02T23:49:30.648005Z | |
38 | +7916 | |
39 | +osimons | |
40 | + | |
41 | + | |
42 | + | |
43 | + | |
44 | + | |
45 | + | |
46 | + | |
47 | + | |
48 | + | |
49 | + | |
50 | + | |
51 | + | |
52 | + | |
53 | + | |
54 | + | |
55 | + | |
56 | + | |
57 | + | |
58 | + | |
59 | + | |
60 | + | |
61 | +13540 | |
62 | + | |
63 | +web_ui.py | |
64 | +file | |
65 | + | |
66 | + | |
67 | + | |
68 | + | |
69 | +2011-03-18T10:32:52.709335Z | |
70 | +d0c5da523bf195ae010b60680208acf9 | |
71 | +2011-03-18T10:32:52.709335Z | |
72 | +9970 | |
73 | +osimons | |
74 | + | |
75 | + | |
76 | + | |
77 | + | |
78 | + | |
79 | + | |
80 | + | |
81 | + | |
82 | + | |
83 | + | |
84 | + | |
85 | + | |
86 | + | |
87 | + | |
88 | + | |
89 | + | |
90 | + | |
91 | + | |
92 | + | |
93 | + | |
94 | + | |
95 | +8858 | |
96 | + | |
97 | +tests | |
98 | +dir | |
99 | + | |
100 | +htdocs | |
101 | +dir | |
102 | + | |
103 | +json_rpc.py | |
104 | +file | |
105 | + | |
106 | + | |
107 | + | |
108 | + | |
109 | +2010-05-17T21:11:27.656010Z | |
110 | +86202cbe15da2a8766ebf3f780659802 | |
111 | +2010-05-17T21:11:27.656010Z | |
112 | +7957 | |
113 | +osimons | |
114 | + | |
115 | + | |
116 | + | |
117 | + | |
118 | + | |
119 | + | |
120 | + | |
121 | + | |
122 | + | |
123 | + | |
124 | + | |
125 | + | |
126 | + | |
127 | + | |
128 | + | |
129 | + | |
130 | + | |
131 | + | |
132 | + | |
133 | + | |
134 | + | |
135 | +10004 | |
136 | + | |
137 | +util.py | |
138 | +file | |
139 | + | |
140 | + | |
141 | + | |
142 | + | |
143 | +2010-05-02T23:49:30.648005Z | |
144 | +e33ca7ad8238685298c9383e33702e8a | |
145 | +2010-05-02T23:49:30.648005Z | |
146 | +7916 | |
147 | +osimons | |
148 | + | |
149 | + | |
150 | + | |
151 | + | |
152 | + | |
153 | + | |
154 | + | |
155 | + | |
156 | + | |
157 | + | |
158 | + | |
159 | + | |
160 | + | |
161 | + | |
162 | + | |
163 | + | |
164 | + | |
165 | + | |
166 | + | |
167 | + | |
168 | + | |
169 | +1837 | |
170 | + | |
171 | +__init__.py | |
172 | +file | |
173 | + | |
174 | + | |
175 | + | |
176 | + | |
177 | +2010-05-02T23:49:30.648005Z | |
178 | +3526fb2c156743b0fde09f8034dc2c25 | |
179 | +2010-05-02T23:49:30.648005Z | |
180 | +7916 | |
181 | +osimons | |
182 | + | |
183 | + | |
184 | + | |
185 | + | |
186 | + | |
187 | + | |
188 | + | |
189 | + | |
190 | + | |
191 | + | |
192 | + | |
193 | + | |
194 | + | |
195 | + | |
196 | + | |
197 | + | |
198 | + | |
199 | + | |
200 | + | |
201 | + | |
202 | + | |
203 | +665 | |
204 | + | |
205 | +ticket.py | |
206 | +file | |
207 | + | |
208 | + | |
209 | + | |
210 | + | |
211 | +2011-03-02T03:40:36.325896Z | |
212 | +6449c7c5ee2a0c957593fae79cef15da | |
213 | +2011-03-02T03:40:36.325896Z | |
214 | +9912 | |
215 | +osimons | |
216 | + | |
217 | + | |
218 | + | |
219 | + | |
220 | + | |
221 | + | |
222 | + | |
223 | + | |
224 | + | |
225 | + | |
226 | + | |
227 | + | |
228 | + | |
229 | + | |
230 | + | |
231 | + | |
232 | + | |
233 | + | |
234 | + | |
235 | + | |
236 | + | |
237 | +20985 | |
238 | + | |
239 | +wiki.py | |
240 | +file | |
241 | + | |
242 | + | |
243 | + | |
244 | + | |
245 | +2011-02-04T00:18:16.742902Z | |
246 | +3d143d6f2bbe9f57d765e5cacf38d6f0 | |
247 | +2011-02-04T00:18:16.742902Z | |
248 | +9818 | |
249 | +osimons | |
250 | + | |
251 | + | |
252 | + | |
253 | + | |
254 | + | |
255 | + | |
256 | + | |
257 | + | |
258 | + | |
259 | + | |
260 | + | |
261 | + | |
262 | + | |
263 | + | |
264 | + | |
265 | + | |
266 | + | |
267 | + | |
268 | + | |
269 | + | |
270 | + | |
271 | +8836 | |
272 | + | |
273 | +xml_rpc.py | |
274 | +file | |
275 | + | |
276 | + | |
277 | + | |
278 | + | |
279 | +2010-10-27T22:10:47.312851Z | |
280 | +fc0ba6c11ad9a1400bde9f5d3f2ae240 | |
281 | +2010-10-27T22:10:47.312851Z | |
282 | +9360 | |
283 | +osimons | |
284 | + | |
285 | + | |
286 | + | |
287 | + | |
288 | + | |
289 | + | |
290 | + | |
291 | + | |
292 | + | |
293 | + | |
294 | + | |
295 | + | |
296 | + | |
297 | + | |
298 | + | |
299 | + | |
300 | + | |
301 | + | |
302 | + | |
303 | + | |
304 | + | |
305 | +7596 | |
306 | + | |
307 | +search.py | |
308 | +file | |
309 | + | |
310 | + | |
311 | + | |
312 | + | |
313 | +2010-05-02T23:49:30.648005Z | |
314 | +11f818f69a8a474437b854a392d00d76 | |
315 | +2010-05-02T23:49:30.648005Z | |
316 | +7916 | |
317 | +osimons | |
318 | + | |
319 | + | |
320 | + | |
321 | + | |
322 | + | |
323 | + | |
324 | + | |
325 | + | |
326 | + | |
327 | + | |
328 | + | |
329 | + | |
330 | + | |
331 | + | |
332 | + | |
333 | + | |
334 | + | |
335 | + | |
336 | + | |
337 | + | |
338 | + | |
339 | +2207 | |
340 | + | |
341 | +templates | |
342 | +dir | |
343 | + |
@@ -0,0 +1,24 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +from tracrpc.api import * | |
10 | +from tracrpc.json_rpc import * | |
11 | +from tracrpc.xml_rpc import * | |
12 | +from tracrpc.web_ui import * | |
13 | +from tracrpc.ticket import * | |
14 | +from tracrpc.wiki import * | |
15 | +from tracrpc.search import * | |
16 | + | |
17 | +__author__ = ['Alec Thomas <alec@swapoff.org>', | |
18 | + 'Odd Simon Simonsen <simon-code@bvnetwork.no>'] | |
19 | +__license__ = 'BSD' | |
20 | + | |
21 | +try: | |
22 | + __version__ = __import__('pkg_resources').get_distribution('TracXMLRPC').version | |
23 | +except (ImportError, pkg_resources.DistributionNotFound): | |
24 | + pass |
@@ -0,0 +1,339 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +import inspect | |
10 | +import types | |
11 | +from datetime import datetime | |
12 | +import xmlrpclib | |
13 | + | |
14 | +from trac.core import * | |
15 | +from trac.perm import IPermissionRequestor | |
16 | + | |
17 | +__all__ = ['expose_rpc', 'IRPCProtocol', 'IXMLRPCHandler', 'AbstractRPCHandler', | |
18 | + 'Method', 'XMLRPCSystem', 'Binary', 'RPCError', 'MethodNotFound', | |
19 | + 'ProtocolException', 'ServiceException'] | |
20 | + | |
21 | +class Binary(xmlrpclib.Binary): | |
22 | + """ RPC Binary type. Currently == xmlrpclib.Binary. """ | |
23 | + pass | |
24 | + | |
25 | +#---------------------------------------------------------------- | |
26 | +# RPC Exception classes | |
27 | +#---------------------------------------------------------------- | |
28 | +class RPCError(TracError): | |
29 | + """ Error class for general RPC-related errors. """ | |
30 | + | |
31 | +class MethodNotFound(RPCError): | |
32 | + """ Error to raise when requested method is not found. """ | |
33 | + | |
34 | +class _CompositeRpcError(RPCError): | |
35 | + def __init__(self, details, title=None, show_traceback=False): | |
36 | + if isinstance(details, Exception): | |
37 | + self._exc = details | |
38 | + message = unicode(details) | |
39 | + else : | |
40 | + self._exc = None | |
41 | + message = details | |
42 | + RPCError.__init__(self, message, title, show_traceback) | |
43 | + def __unicode__(self): | |
44 | + return u"%s details : %s" % (self.__class__.__name__, self.message) | |
45 | + | |
46 | +class ProtocolException(_CompositeRpcError): | |
47 | + """Protocol could not handle RPC request. Usually this means | |
48 | + that the request has some sort of syntactic error, a library | |
49 | + needed to parse the RPC call is not available, or similar errors.""" | |
50 | + | |
51 | +class ServiceException(_CompositeRpcError): | |
52 | + """The called method threw an exception. Helpful to identify bugs ;o)""" | |
53 | + | |
54 | +RPC_TYPES = {int: 'int', bool: 'boolean', str: 'string', float: 'double', | |
55 | + datetime: 'dateTime.iso8601', Binary: 'base64', | |
56 | + list: 'array', dict: 'struct', None : 'int'} | |
57 | + | |
58 | +def expose_rpc(permission, return_type, *arg_types): | |
59 | + """ Decorator for exposing a method as an RPC call with the given | |
60 | + signature. """ | |
61 | + def decorator(func): | |
62 | + if not hasattr(func, '_xmlrpc_signatures'): | |
63 | + func._xmlrpc_signatures = [] | |
64 | + func._xml_rpc_permission = permission | |
65 | + func._xmlrpc_signatures.append((return_type,) + tuple(arg_types)) | |
66 | + return func | |
67 | + return decorator | |
68 | + | |
69 | + | |
70 | +class IRPCProtocol(Interface): | |
71 | + | |
72 | + def rpc_info(): | |
73 | + """ Returns a tuple of (name, docs). Method provides | |
74 | + general information about the protocol used for the RPC HTML view. | |
75 | + | |
76 | + name: Shortname like 'XML-RPC'. | |
77 | + docs: Documentation for the protocol. | |
78 | + """ | |
79 | + | |
80 | + def rpc_match(): | |
81 | + """ Return an iterable of (path_item, content_type) combinations that | |
82 | + will be handled by the protocol. | |
83 | + | |
84 | + path_item: Single word to use for matching against | |
85 | + (/login)?/<path_item>. Answer to 'rpc' only if possible. | |
86 | + content_type: Starts-with check of 'Content-Type' request header. """ | |
87 | + | |
88 | + def parse_rpc_request(req, content_type): | |
89 | + """ Parse RPC requests. | |
90 | + | |
91 | + req : HTTP request object | |
92 | + content_type : Input MIME type | |
93 | + | |
94 | + Return a dictionary with the following keys set. All the other | |
95 | + values included in this mapping will be ignored by the core | |
96 | + RPC subsystem, will be protocol-specific, and SHOULD NOT be | |
97 | + needed in order to invoke a given method. | |
98 | + | |
99 | + method (MANDATORY): target method name (e.g. 'ticket.get') | |
100 | + params (OPTIONAL) : a tuple containing input positional arguments | |
101 | + headers (OPTIONAL) : if the protocol supports custom headers set | |
102 | + by the client, then this value SHOULD be a | |
103 | + dictionary binding `header name` to `value`. | |
104 | + However, protocol handlers as well as target | |
105 | + RPC methods *MUST (SHOULD ?) NOT* rely on | |
106 | + specific values assigned to a particular | |
107 | + header in order to send a response back | |
108 | + to the client. | |
109 | + mimetype : request MIME-type. This value will be set | |
110 | + by core RPC components after calling | |
111 | + this method so, please, ignore | |
112 | + | |
113 | + If the request cannot be parsed this method *MUST* raise | |
114 | + an instance of `ProtocolException` optionally wrapping another | |
115 | + exception containing details about the failure. | |
116 | + """ | |
117 | + | |
118 | + def send_rpc_result(req, result): | |
119 | + """Serialize the result of the RPC call and send it back to | |
120 | + the client. | |
121 | + | |
122 | + req : Request object. The same mapping returned by | |
123 | + `parse_rpc_request` can be accessed through | |
124 | + `req.rpc` (see above). | |
125 | + result : The value returned by the target RPC method | |
126 | + """ | |
127 | + | |
128 | + def send_rpc_error(req, rpcreq, e): | |
129 | + """Send a fault message back to the caller. Exception type | |
130 | + and message are used for this purpose. This method *SHOULD* | |
131 | + handle `RPCError`, `PermissionError`, `ResourceNotFound` and | |
132 | + their subclasses. This method is *ALWAYS* called from within | |
133 | + an exception handler. | |
134 | + | |
135 | + req : Request object. The same mapping returned by | |
136 | + `parse_rpc_request` can be accessed through | |
137 | + `req.rpc` (see above). | |
138 | + e : exception object describing the failure | |
139 | + """ | |
140 | + | |
141 | +class IXMLRPCHandler(Interface): | |
142 | + | |
143 | + def xmlrpc_namespace(): | |
144 | + """ Provide the namespace in which a set of methods lives. | |
145 | + This can be overridden if the 'name' element is provided by | |
146 | + xmlrpc_methods(). """ | |
147 | + | |
148 | + def xmlrpc_methods(): | |
149 | + """ Return an iterator of (permission, signatures, callable[, name]), | |
150 | + where callable is exposed via XML-RPC if the authenticated user has the | |
151 | + appropriate permission. | |
152 | + | |
153 | + The callable itself can be a method or a normal method. The first | |
154 | + argument passed will always be a request object. The XMLRPCSystem | |
155 | + performs some extra magic to remove the "self" and "req" arguments when | |
156 | + listing the available methods. | |
157 | + | |
158 | + Signatures is a list of XML-RPC introspection signatures for this | |
159 | + method. Each signature is a tuple consisting of the return type | |
160 | + followed by argument types. | |
161 | + """ | |
162 | + | |
163 | +class AbstractRPCHandler(Component): | |
164 | + implements(IXMLRPCHandler) | |
165 | + abstract = True | |
166 | + | |
167 | + def _init_methods(self): | |
168 | + self._rpc_methods = [] | |
169 | + for name, val in inspect.getmembers(self): | |
170 | + if hasattr(val, '_xmlrpc_signatures'): | |
171 | + self._rpc_methods.append((val._xml_rpc_permission, val._xmlrpc_signatures, val, name)) | |
172 | + | |
173 | + def xmlrpc_methods(self): | |
174 | + if not hasattr(self, '_rpc_methods'): | |
175 | + self._init_methods() | |
176 | + return self._rpc_methods | |
177 | + | |
178 | + | |
179 | +class Method(object): | |
180 | + """ Represents an XML-RPC exposed method. """ | |
181 | + def __init__(self, provider, permission, signatures, callable, name = None): | |
182 | + """ Accept a signature in the form returned by xmlrpc_methods. """ | |
183 | + self.permission = permission | |
184 | + self.callable = callable | |
185 | + self.rpc_signatures = signatures | |
186 | + self.description = inspect.getdoc(callable) | |
187 | + if name is None: | |
188 | + self.name = provider.xmlrpc_namespace() + '.' + callable.__name__ | |
189 | + else: | |
190 | + self.name = provider.xmlrpc_namespace() + '.' + name | |
191 | + self.namespace = provider.xmlrpc_namespace() | |
192 | + self.namespace_description = inspect.getdoc(provider) | |
193 | + | |
194 | + def __call__(self, req, args): | |
195 | + if self.permission: | |
196 | + req.perm.assert_permission(self.permission) | |
197 | + result = self.callable(req, *args) | |
198 | + # If result is null, return a zero | |
199 | + if result is None: | |
200 | + result = 0 | |
201 | + elif isinstance(result, dict): | |
202 | + pass | |
203 | + elif not isinstance(result, basestring): | |
204 | + # Try and convert result to a list | |
205 | + try: | |
206 | + result = [i for i in result] | |
207 | + except TypeError: | |
208 | + pass | |
209 | + return (result,) | |
210 | + | |
211 | + def _get_signature(self): | |
212 | + """ Return the signature of this method. """ | |
213 | + if hasattr(self, '_signature'): | |
214 | + return self._signature | |
215 | + fullargspec = inspect.getargspec(self.callable) | |
216 | + argspec = fullargspec[0] | |
217 | + assert argspec[0:2] == ['self', 'req'] or argspec[0] == 'req', \ | |
218 | + 'Invalid argspec %s for %s' % (argspec, self.name) | |
219 | + while argspec and (argspec[0] in ('self', 'req')): | |
220 | + argspec.pop(0) | |
221 | + argspec.reverse() | |
222 | + defaults = fullargspec[3] | |
223 | + if not defaults: | |
224 | + defaults = [] | |
225 | + else: | |
226 | + defaults = list(defaults) | |
227 | + args = [] | |
228 | + sig = [] | |
229 | + for sigcand in self.xmlrpc_signatures(): | |
230 | + if len(sig) < len(sigcand): | |
231 | + sig = sigcand | |
232 | + sig = list(sig) | |
233 | + for arg in argspec: | |
234 | + if defaults: | |
235 | + value = defaults.pop() | |
236 | + if type(value) is str: | |
237 | + if '"' in value: | |
238 | + value = "'%s'" % value | |
239 | + else: | |
240 | + value = '"%s"' % value | |
241 | + arg += '=%s' % value | |
242 | + args.insert(0, RPC_TYPES[sig.pop()] + ' ' + arg) | |
243 | + self._signature = '%s %s(%s)' % (RPC_TYPES[sig.pop()], self.name, ', '.join(args)) | |
244 | + return self._signature | |
245 | + | |
246 | + signature = property(_get_signature) | |
247 | + | |
248 | + def xmlrpc_signatures(self): | |
249 | + """ Signature as an XML-RPC 'signature'. """ | |
250 | + return self.rpc_signatures | |
251 | + | |
252 | + | |
253 | +class XMLRPCSystem(Component): | |
254 | + """ Core of the RPC system. """ | |
255 | + implements(IPermissionRequestor, IXMLRPCHandler) | |
256 | + | |
257 | + method_handlers = ExtensionPoint(IXMLRPCHandler) | |
258 | + | |
259 | + def __init__(self): | |
260 | + self.env.systeminfo.append(('RPC', | |
261 | + __import__('tracrpc', ['__version__']).__version__)) | |
262 | + | |
263 | + # IPermissionRequestor methods | |
264 | + def get_permission_actions(self): | |
265 | + yield 'XML_RPC' | |
266 | + | |
267 | + # IXMLRPCHandler methods | |
268 | + def xmlrpc_namespace(self): | |
269 | + return 'system' | |
270 | + | |
271 | + def xmlrpc_methods(self): | |
272 | + yield ('XML_RPC', ((list, list),), self.multicall) | |
273 | + yield ('XML_RPC', ((list,),), self.listMethods) | |
274 | + yield ('XML_RPC', ((str, str),), self.methodHelp) | |
275 | + yield ('XML_RPC', ((list, str),), self.methodSignature) | |
276 | + yield ('XML_RPC', ((list,),), self.getAPIVersion) | |
277 | + | |
278 | + def get_method(self, method): | |
279 | + """ Get an RPC signature by full name. """ | |
280 | + for provider in self.method_handlers: | |
281 | + for candidate in provider.xmlrpc_methods(): | |
282 | + #self.env.log.debug(candidate) | |
283 | + p = Method(provider, *candidate) | |
284 | + if p.name == method: | |
285 | + return p | |
286 | + raise MethodNotFound('RPC method "%s" not found' % method) | |
287 | + | |
288 | + # Exported methods | |
289 | + def all_methods(self, req): | |
290 | + """ List all methods exposed via RPC. Returns a list of Method objects. """ | |
291 | + for provider in self.method_handlers: | |
292 | + for candidate in provider.xmlrpc_methods(): | |
293 | + # Expand all fields of method description | |
294 | + yield Method(provider, *candidate) | |
295 | + | |
296 | + def multicall(self, req, signatures): | |
297 | + """ Takes an array of RPC calls encoded as structs of the form (in | |
298 | + a Pythonish notation here): `{'methodName': string, 'params': array}`. | |
299 | + For JSON-RPC multicall, signatures is an array of regular method call | |
300 | + structs, and result is an array of return structures. | |
301 | + """ | |
302 | + for signature in signatures: | |
303 | + try: | |
304 | + yield self.get_method(signature['methodName'])(req, signature['params']) | |
305 | + except Exception, e: | |
306 | + yield e | |
307 | + | |
308 | + def listMethods(self, req): | |
309 | + """ This method returns a list of strings, one for each (non-system) | |
310 | + method supported by the RPC server. """ | |
311 | + for method in self.all_methods(req): | |
312 | + yield method.name | |
313 | + | |
314 | + def methodHelp(self, req, method): | |
315 | + """ This method takes one parameter, the name of a method implemented | |
316 | + by the RPC server. It returns a documentation string describing the | |
317 | + use of that method. If no such string is available, an empty string is | |
318 | + returned. The documentation string may contain HTML markup. """ | |
319 | + p = self.get_method(method) | |
320 | + return '\n'.join((p.signature, '', p.description)) | |
321 | + | |
322 | + def methodSignature(self, req, method): | |
323 | + """ This method takes one parameter, the name of a method implemented | |
324 | + by the RPC server. | |
325 | + | |
326 | + It returns an array of possible signatures for this method. A signature | |
327 | + is an array of types. The first of these types is the return type of | |
328 | + the method, the rest are parameters. """ | |
329 | + p = self.get_method(method) | |
330 | + return [','.join([RPC_TYPES[x] for x in sig]) for sig in p.xmlrpc_signatures()] | |
331 | + | |
332 | + def getAPIVersion(self, req): | |
333 | + """ Returns a list with three elements. First element is the | |
334 | + epoch (0=Trac 0.10, 1=Trac 0.11 or higher). Second element is the major | |
335 | + version number, third is the minor. Changes to the major version | |
336 | + indicate API breaking changes, while minor version changes are simple | |
337 | + additions, bug fixes, etc. """ | |
338 | + import tracrpc | |
339 | + return map(int, tracrpc.__version__.split('-')[0].split('.')) |
@@ -0,0 +1,235 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import datetime | |
9 | +from itertools import izip | |
10 | +import re | |
11 | +from types import GeneratorType | |
12 | + | |
13 | +from trac.core import * | |
14 | +from trac.perm import PermissionError | |
15 | +from trac.resource import ResourceNotFound | |
16 | +from trac.util.datefmt import utc | |
17 | +from trac.util.text import to_unicode | |
18 | +from trac.web.api import RequestDone | |
19 | + | |
20 | +from tracrpc.api import IRPCProtocol, XMLRPCSystem, Binary, \ | |
21 | + RPCError, MethodNotFound, ProtocolException | |
22 | +from tracrpc.util import exception_to_unicode, empty, prepare_docs | |
23 | + | |
24 | +__all__ = ['JsonRpcProtocol'] | |
25 | + | |
26 | +try: | |
27 | + import json | |
28 | + if not (hasattr(json, 'JSONEncoder') \ | |
29 | + and hasattr(json, 'JSONDecoder')): | |
30 | + raise AttributeError("Incorrect JSON library found.") | |
31 | +except (ImportError, AttributeError): | |
32 | + try: | |
33 | + import simplejson as json | |
34 | + except ImportError: | |
35 | + json = None | |
36 | + __all__ = [] | |
37 | + | |
38 | +if json: | |
39 | + class TracRpcJSONEncoder(json.JSONEncoder): | |
40 | + """ Extending the JSON encoder to support some additional types: | |
41 | + 1. datetime.datetime => {'__jsonclass__': ["datetime", "<rfc3339str>"]} | |
42 | + 2. tracrpc.api.Binary => {'__jsonclass__': ["binary", "<base64str>"]} | |
43 | + 3. empty => '' """ | |
44 | + | |
45 | + def default(self, obj): | |
46 | + if isinstance(obj, datetime.datetime): | |
47 | + # http://www.ietf.org/rfc/rfc3339.txt | |
48 | + return {'__jsonclass__': ["datetime", | |
49 | + obj.strftime('%Y-%m-%dT%H:%M:%S')]} | |
50 | + elif isinstance(obj, Binary): | |
51 | + return {'__jsonclass__': ["binary", | |
52 | + obj.data.encode("base64")]} | |
53 | + elif obj is empty: | |
54 | + return '' | |
55 | + else: | |
56 | + return json.JSONEncoder(self, obj) | |
57 | + | |
58 | + class TracRpcJSONDecoder(json.JSONDecoder): | |
59 | + """ Extending the JSON decoder to support some additional types: | |
60 | + 1. {'__jsonclass__': ["datetime", "<rfc3339str>"]} => datetime.datetime | |
61 | + 2. {'__jsonclass__': ["binary", "<base64str>"]} => tracrpc.api.Binary """ | |
62 | + | |
63 | + dt = re.compile( | |
64 | + '^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,}))?') | |
65 | + | |
66 | + def _normalize(self, obj): | |
67 | + """ Helper to traverse JSON decoded object for custom types. """ | |
68 | + if isinstance(obj, tuple): | |
69 | + return tuple(self._normalize(item) for item in obj) | |
70 | + elif isinstance(obj, list): | |
71 | + return [self._normalize(item) for item in obj] | |
72 | + elif isinstance(obj, dict): | |
73 | + if obj.keys() == ['__jsonclass__']: | |
74 | + kind, val = obj['__jsonclass__'] | |
75 | + if kind == 'datetime': | |
76 | + dt = self.dt.match(val) | |
77 | + if not dt: | |
78 | + raise Exception( | |
79 | + "Invalid datetime string (%s)" % val) | |
80 | + dt = tuple([int(i) for i in dt.groups() if i]) | |
81 | + kw_args = {'tzinfo': utc} | |
82 | + return datetime.datetime(*dt, **kw_args) | |
83 | + elif kind == 'binary': | |
84 | + try: | |
85 | + bin = val.decode("base64") | |
86 | + return Binary(bin) | |
87 | + except: | |
88 | + raise Exception("Invalid base64 string") | |
89 | + else: | |
90 | + raise Exception("Unknown __jsonclass__: %s" % kind) | |
91 | + else: | |
92 | + return dict(self._normalize(obj.items())) | |
93 | + elif isinstance(obj, basestring): | |
94 | + return to_unicode(obj) | |
95 | + else: | |
96 | + return obj | |
97 | + | |
98 | + def decode(self, obj, *args, **kwargs): | |
99 | + obj = json.JSONDecoder.decode(self, obj, *args, **kwargs) | |
100 | + return self._normalize(obj) | |
101 | + | |
102 | + class JsonProtocolException(ProtocolException): | |
103 | + """Impossible to handle JSON-RPC request.""" | |
104 | + def __init__(self, details, code=-32603, title=None, show_traceback=False): | |
105 | + ProtocolException.__init__(self, details, title, show_traceback) | |
106 | + self.code = code | |
107 | + | |
108 | + class JsonRpcProtocol(Component): | |
109 | + r""" | |
110 | + Example `POST` request using `curl` with `Content-Type` header | |
111 | + and body: | |
112 | + | |
113 | + {{{ | |
114 | + user: ~ > cat body.json | |
115 | + {"params": ["WikiStart"], "method": "wiki.getPage", "id": 123} | |
116 | + user: ~ > curl -H "Content-Type: application/json" --data @body.json ${req.abs_href.rpc()} | |
117 | + {"id": 123, "error": null, "result": "= Welcome to.... | |
118 | + }}} | |
119 | + | |
120 | + Implementation details: | |
121 | + | |
122 | + * JSON-RPC has no formalized type system, so a class-hint system is used | |
123 | + for input and output of non-standard types: | |
124 | + * `{"__jsonclass__": ["datetime", "YYYY-MM-DDTHH:MM:SS"]} => DateTime (UTC)` | |
125 | + * `{"__jsonclass__": ["binary", "<base64-encoded>"]} => Binary` | |
126 | + * `"id"` is optional, and any marker value received with a | |
127 | + request is returned with the response. | |
128 | + """ | |
129 | + | |
130 | + implements(IRPCProtocol) | |
131 | + | |
132 | + # IRPCProtocol methods | |
133 | + | |
134 | + def rpc_info(self): | |
135 | + return ('JSON-RPC', prepare_docs(self.__doc__)) | |
136 | + | |
137 | + def rpc_match(self): | |
138 | + yield('rpc', 'application/json') | |
139 | + # Legacy path - provided for backwards compatibility: | |
140 | + yield ('jsonrpc', 'application/json') | |
141 | + | |
142 | + def parse_rpc_request(self, req, content_type): | |
143 | + """ Parse JSON-RPC requests""" | |
144 | + if not json: | |
145 | + self.log.debug("RPC(json) call ignored (not available).") | |
146 | + raise JsonProtocolException("Error: JSON-RPC not available.\n") | |
147 | + try: | |
148 | + data = json.load(req, cls=TracRpcJSONDecoder) | |
149 | + self.log.info("RPC(json) JSON-RPC request ID : %s.", data.get('id')) | |
150 | + if data.get('method') == 'system.multicall': | |
151 | + # Prepare for multicall | |
152 | + self.log.debug("RPC(json) Multicall request %s", data) | |
153 | + params = data.get('params', []) | |
154 | + for signature in params : | |
155 | + signature['methodName'] = signature.get('method', '') | |
156 | + data['params'] = [params] | |
157 | + return data | |
158 | + except Exception, e: | |
159 | + # Abort with exception - no data can be read | |
160 | + self.log.error("RPC(json) decode error %s", | |
161 | + exception_to_unicode(e, traceback=True)) | |
162 | + raise JsonProtocolException(e, -32700) | |
163 | + | |
164 | + def send_rpc_result(self, req, result): | |
165 | + """Send JSON-RPC response back to the caller.""" | |
166 | + rpcreq = req.rpc | |
167 | + r_id = rpcreq.get('id') | |
168 | + try: | |
169 | + if rpcreq.get('method') == 'system.multicall': | |
170 | + # Custom multicall | |
171 | + args = (rpcreq.get('params') or [[]])[0] | |
172 | + mcresults = [self._json_result( | |
173 | + isinstance(value, Exception) and \ | |
174 | + value or value[0], \ | |
175 | + sig.get('id') or r_id) \ | |
176 | + for sig, value in izip(args, result)] | |
177 | + | |
178 | + response = self._json_result(mcresults, r_id) | |
179 | + else: | |
180 | + response = self._json_result(result, r_id) | |
181 | + try: # JSON encoding | |
182 | + self.log.debug("RPC(json) result: %s" % repr(response)) | |
183 | + response = json.dumps(response, cls=TracRpcJSONEncoder) | |
184 | + except Exception, e: | |
185 | + response = json.dumps(self._json_error(e, r_id=r_id), | |
186 | + cls=TracRpcJSONEncoder) | |
187 | + except Exception, e: | |
188 | + self.log.error("RPC(json) error %s" % exception_to_unicode(e, | |
189 | + traceback=True)) | |
190 | + response = json.dumps(self._json_error(e, r_id=r_id), | |
191 | + cls=TracRpcJSONEncoder) | |
192 | + self._send_response(req, response + '\n', rpcreq['mimetype']) | |
193 | + | |
194 | + def send_rpc_error(self, req, e): | |
195 | + """Send a JSON-RPC fault message back to the caller. """ | |
196 | + rpcreq = req.rpc | |
197 | + r_id = rpcreq.get('id') | |
198 | + response = json.dumps(self._json_error(e, r_id=r_id), \ | |
199 | + cls=TracRpcJSONEncoder) | |
200 | + self._send_response(req, response + '\n', rpcreq['mimetype']) | |
201 | + | |
202 | + # Internal methods | |
203 | + | |
204 | + def _send_response(self, req, response, content_type='application/json'): | |
205 | + self.log.debug("RPC(json) encoded response: %s" % response) | |
206 | + response = to_unicode(response).encode("utf-8") | |
207 | + req.send_response(200) | |
208 | + req.send_header('Content-Type', content_type) | |
209 | + req.send_header('Content-Length', len(response)) | |
210 | + req.end_headers() | |
211 | + req.write(response) | |
212 | + raise RequestDone() | |
213 | + | |
214 | + def _json_result(self, result, r_id=None): | |
215 | + """ Create JSON-RPC response dictionary. """ | |
216 | + if not isinstance(result, Exception): | |
217 | + return {'result': result, 'error': None, 'id': r_id} | |
218 | + else : | |
219 | + return self._json_error(result, r_id=r_id) | |
220 | + | |
221 | + def _json_error(self, e, c=None, r_id=None): | |
222 | + """ Makes a response dictionary that is an error. """ | |
223 | + if isinstance(e, MethodNotFound): | |
224 | + c = -32601 | |
225 | + elif isinstance(e, PermissionError): | |
226 | + c = 403 | |
227 | + elif isinstance(e, ResourceNotFound): | |
228 | + c = 404 | |
229 | + else: | |
230 | + c = c or hasattr(e, 'code') and e.code or -32603 | |
231 | + return {'result': None, 'id': r_id, 'error': { | |
232 | + 'name': hasattr(e, 'name') and e.name or 'JSONRPCError', | |
233 | + 'code': c, | |
234 | + 'message': to_unicode(e)}} | |
235 | + |
@@ -0,0 +1,63 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +from trac.core import * | |
10 | +from trac.search.api import ISearchSource | |
11 | +from trac.search.web_ui import SearchModule | |
12 | +from trac.util.compat import set | |
13 | + | |
14 | +from tracrpc.api import IXMLRPCHandler | |
15 | + | |
16 | +__all__ = ['SearchRPC'] | |
17 | + | |
18 | +class SearchRPC(Component): | |
19 | + """ Search Trac. """ | |
20 | + implements(IXMLRPCHandler) | |
21 | + | |
22 | + search_sources = ExtensionPoint(ISearchSource) | |
23 | + | |
24 | + # IXMLRPCHandler methods | |
25 | + def xmlrpc_namespace(self): | |
26 | + return 'search' | |
27 | + | |
28 | + def xmlrpc_methods(self): | |
29 | + yield ('SEARCH_VIEW', ((list,),), self.getSearchFilters) | |
30 | + yield ('SEARCH_VIEW', ((list, str), (list, str, list)), self.performSearch) | |
31 | + | |
32 | + # Others | |
33 | + def getSearchFilters(self, req): | |
34 | + """ Retrieve a list of search filters with each element in the form | |
35 | + (name, description). """ | |
36 | + for source in self.search_sources: | |
37 | + for filter in source.get_search_filters(req): | |
38 | + yield filter | |
39 | + | |
40 | + def performSearch(self, req, query, filters=None): | |
41 | + """ Perform a search using the given filters. Defaults to all if not | |
42 | + provided. Results are returned as a list of tuples in the form | |
43 | + (href, title, date, author, excerpt).""" | |
44 | + query = SearchModule(self.env)._get_search_terms(query) | |
45 | + filters_provided = filters is not None | |
46 | + chosen_filters = set(filters or []) | |
47 | + available_filters = [] | |
48 | + for source in self.search_sources: | |
49 | + available_filters += source.get_search_filters(req) | |
50 | + | |
51 | + filters = [f[0] for f in available_filters if f[0] in chosen_filters] | |
52 | + if not filters: | |
53 | + if filters_provided: | |
54 | + return [] | |
55 | + filters = [f[0] for f in available_filters] | |
56 | + self.env.log.debug("Searching with %s" % filters) | |
57 | + | |
58 | + results = [] | |
59 | + for source in self.search_sources: | |
60 | + for result in source.get_search_results(req, query, filters): | |
61 | + results.append(['/'.join(req.base_url.split('/')[0:3]) | |
62 | + + result[0]] + list(result[1:])) | |
63 | + return results |
@@ -0,0 +1,488 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +import inspect | |
10 | +from datetime import datetime | |
11 | + | |
12 | +import genshi | |
13 | + | |
14 | +from trac.attachment import Attachment | |
15 | +from trac.core import * | |
16 | +from trac.perm import PermissionError | |
17 | +from trac.resource import Resource, ResourceNotFound | |
18 | +import trac.ticket.model as model | |
19 | +import trac.ticket.query as query | |
20 | +from trac.ticket.api import TicketSystem | |
21 | +from trac.ticket.notification import TicketNotifyEmail | |
22 | +from trac.ticket.web_ui import TicketModule | |
23 | +from trac.web.chrome import add_warning | |
24 | +from trac.util.datefmt import to_datetime, utc | |
25 | + | |
26 | +from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary | |
27 | +from tracrpc.util import StringIO, to_utimestamp | |
28 | + | |
29 | +__all__ = ['TicketRPC'] | |
30 | + | |
31 | +class TicketRPC(Component): | |
32 | + """ An interface to Trac's ticketing system. """ | |
33 | + | |
34 | + implements(IXMLRPCHandler) | |
35 | + | |
36 | + # IXMLRPCHandler methods | |
37 | + def xmlrpc_namespace(self): | |
38 | + return 'ticket' | |
39 | + | |
40 | + def xmlrpc_methods(self): | |
41 | + yield (None, ((list,), (list, str)), self.query) | |
42 | + yield (None, ((list, datetime),), self.getRecentChanges) | |
43 | + yield (None, ((list, int),), self.getAvailableActions) | |
44 | + yield (None, ((list, int),), self.getActions) | |
45 | + yield (None, ((list, int),), self.get) | |
46 | + yield ('TICKET_CREATE', ((int, str, str), | |
47 | + (int, str, str, dict), | |
48 | + (int, str, str, dict, bool), | |
49 | + (int, str, str, dict, bool, datetime)), | |
50 | + self.create) | |
51 | + yield (None, ((list, int, str), | |
52 | + (list, int, str, dict), | |
53 | + (list, int, str, dict, bool), | |
54 | + (list, int, str, dict, bool, str), | |
55 | + (list, int, str, dict, bool, str, datetime)), | |
56 | + self.update) | |
57 | + yield (None, ((None, int),), self.delete) | |
58 | + yield (None, ((dict, int), (dict, int, int)), self.changeLog) | |
59 | + yield (None, ((list, int),), self.listAttachments) | |
60 | + yield (None, ((Binary, int, str),), self.getAttachment) | |
61 | + yield (None, | |
62 | + ((str, int, str, str, Binary, bool), | |
63 | + (str, int, str, str, Binary)), | |
64 | + self.putAttachment) | |
65 | + yield (None, ((bool, int, str),), self.deleteAttachment) | |
66 | + yield ('TICKET_VIEW', ((list,),), self.getTicketFields) | |
67 | + | |
68 | + # Exported methods | |
69 | + def query(self, req, qstr='status!=closed'): | |
70 | + """ | |
71 | + Perform a ticket query, returning a list of ticket ID's. | |
72 | + All queries will use stored settings for maximum number of results per | |
73 | + page and paging options. Use `max=n` to define number of results to | |
74 | + receive, and use `page=n` to page through larger result sets. Using | |
75 | + `max=0` will turn off paging and return all results. | |
76 | + """ | |
77 | + q = query.Query.from_string(self.env, qstr) | |
78 | + ticket_realm = Resource('ticket') | |
79 | + out = [] | |
80 | + for t in q.execute(req): | |
81 | + tid = t['id'] | |
82 | + if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)): | |
83 | + out.append(tid) | |
84 | + return out | |
85 | + | |
86 | + def getRecentChanges(self, req, since): | |
87 | + """Returns a list of IDs of tickets that have changed since timestamp.""" | |
88 | + since = to_utimestamp(since) | |
89 | + db = self.env.get_db_cnx() | |
90 | + cursor = db.cursor() | |
91 | + cursor.execute('SELECT id FROM ticket' | |
92 | + ' WHERE changetime >= %s', (since,)) | |
93 | + result = [] | |
94 | + ticket_realm = Resource('ticket') | |
95 | + for row in cursor: | |
96 | + tid = int(row[0]) | |
97 | + if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)): | |
98 | + result.append(tid) | |
99 | + return result | |
100 | + | |
101 | + def getAvailableActions(self, req, id): | |
102 | + """ Deprecated - will be removed. Replaced by `getActions()`. """ | |
103 | + self.log.warning("Rpc ticket.getAvailableActions is deprecated") | |
104 | + return [action[0] for action in self.getActions(req, id)] | |
105 | + | |
106 | + def getActions(self, req, id): | |
107 | + """Returns the actions that can be performed on the ticket as a list of | |
108 | + `[action, label, hints, [input_fields]]` elements, where `input_fields` is | |
109 | + a list of `[name, value, [options]]` for any required action inputs.""" | |
110 | + ts = TicketSystem(self.env) | |
111 | + t = model.Ticket(self.env, id) | |
112 | + actions = [] | |
113 | + for action in ts.get_available_actions(req, t): | |
114 | + fragment = genshi.builder.Fragment() | |
115 | + hints = [] | |
116 | + first_label = None | |
117 | + for controller in ts.action_controllers: | |
118 | + if action in [c_action for c_weight, c_action \ | |
119 | + in controller.get_ticket_actions(req, t)]: | |
120 | + label, widget, hint = \ | |
121 | + controller.render_ticket_action_control(req, t, action) | |
122 | + fragment += widget | |
123 | + hints.append(hint) | |
124 | + first_label = first_label == None and label or first_label | |
125 | + controls = [] | |
126 | + for elem in fragment.children: | |
127 | + if not isinstance(elem, genshi.builder.Element): | |
128 | + continue | |
129 | + if elem.tag == 'input': | |
130 | + controls.append((elem.attrib.get('name'), | |
131 | + elem.attrib.get('value'), [])) | |
132 | + elif elem.tag == 'select': | |
133 | + value = '' | |
134 | + options = [] | |
135 | + for opt in elem.children: | |
136 | + if not (opt.tag == 'option' and opt.children): | |
137 | + continue | |
138 | + option = opt.children[0] | |
139 | + options.append(option) | |
140 | + if opt.attrib.get('selected'): | |
141 | + value = option | |
142 | + controls.append((elem.attrib.get('name'), | |
143 | + value, options)) | |
144 | + actions.append((action, first_label, ". ".join(hints) + '.', controls)) | |
145 | + return actions | |
146 | + | |
147 | + def get(self, req, id): | |
148 | + """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ | |
149 | + t = model.Ticket(self.env, id) | |
150 | + req.perm(t.resource).require('TICKET_VIEW') | |
151 | + t['_ts'] = str(t.time_changed) | |
152 | + return (t.id, t.time_created, t.time_changed, t.values) | |
153 | + | |
154 | + def create(self, req, summary, description, attributes={}, notify=False, when=None): | |
155 | + """ Create a new ticket, returning the ticket ID. | |
156 | + Overriding 'when' requires admin permission. """ | |
157 | + t = model.Ticket(self.env) | |
158 | + t['summary'] = summary | |
159 | + t['description'] = description | |
160 | + t['reporter'] = req.authname | |
161 | + for k, v in attributes.iteritems(): | |
162 | + t[k] = v | |
163 | + t['status'] = 'new' | |
164 | + t['resolution'] = '' | |
165 | + # custom create timestamp? | |
166 | + if when and not 'TICKET_ADMIN' in req.perm: | |
167 | + self.log.warn("RPC ticket.create: %r not allowed to create with " | |
168 | + "non-current timestamp (%r)", req.authname, when) | |
169 | + when = None | |
170 | + t.insert(when=when) | |
171 | + # Call ticket change listeners | |
172 | + ts = TicketSystem(self.env) | |
173 | + for listener in ts.change_listeners: | |
174 | + listener.ticket_created(t) | |
175 | + if notify: | |
176 | + try: | |
177 | + tn = TicketNotifyEmail(self.env) | |
178 | + tn.notify(t, newticket=True) | |
179 | + except Exception, e: | |
180 | + self.log.exception("Failure sending notification on creation " | |
181 | + "of ticket #%s: %s" % (t.id, e)) | |
182 | + return t.id | |
183 | + | |
184 | + def update(self, req, id, comment, attributes={}, notify=False, author='', when=None): | |
185 | + """ Update a ticket, returning the new ticket in the same form as | |
186 | + get(). 'New-style' call requires two additional items in attributes: | |
187 | + (1) 'action' for workflow support (including any supporting fields | |
188 | + as retrieved by getActions()), | |
189 | + (2) '_ts' changetime token for detecting update collisions (as received | |
190 | + from get() or update() calls). | |
191 | + ''Calling update without 'action' and '_ts' changetime token is | |
192 | + deprecated, and will raise errors in a future version.'' """ | |
193 | + t = model.Ticket(self.env, id) | |
194 | + # custom author? | |
195 | + if author and not (req.authname == 'anonymous' \ | |
196 | + or 'TICKET_ADMIN' in req.perm(t.resource)): | |
197 | + # only allow custom author if anonymous is permitted or user is admin | |
198 | + self.log.warn("RPC ticket.update: %r not allowed to change author " | |
199 | + "to %r for comment on #%d", req.authname, author, id) | |
200 | + author = '' | |
201 | + author = author or req.authname | |
202 | + # custom change timestamp? | |
203 | + if when and not 'TICKET_ADMIN' in req.perm(t.resource): | |
204 | + self.log.warn("RPC ticket.update: %r not allowed to update #%d with " | |
205 | + "non-current timestamp (%r)", author, id, when) | |
206 | + when = None | |
207 | + when = when or to_datetime(None, utc) | |
208 | + # and action... | |
209 | + if not 'action' in attributes: | |
210 | + # FIXME: Old, non-restricted update - remove soon! | |
211 | + self.log.warning("Rpc ticket.update for ticket %d by user %s " \ | |
212 | + "has no workflow 'action'." % (id, req.authname)) | |
213 | + req.perm(t.resource).require('TICKET_MODIFY') | |
214 | + time_changed = attributes.pop('_ts', None) | |
215 | + if time_changed and str(time_changed) != str(t.time_changed): | |
216 | + raise TracError("Ticket has been updated since last get().") | |
217 | + for k, v in attributes.iteritems(): | |
218 | + t[k] = v | |
219 | + t.save_changes(author, comment, when=when) | |
220 | + else: | |
221 | + ts = TicketSystem(self.env) | |
222 | + tm = TicketModule(self.env) | |
223 | + # TODO: Deprecate update without time_changed timestamp | |
224 | + time_changed = str(attributes.pop('_ts', t.time_changed)) | |
225 | + action = attributes.get('action') | |
226 | + avail_actions = ts.get_available_actions(req, t) | |
227 | + if not action in avail_actions: | |
228 | + raise TracError("Rpc: Ticket %d by %s " \ | |
229 | + "invalid action '%s'" % (id, req.authname, action)) | |
230 | + controllers = list(tm._get_action_controllers(req, t, action)) | |
231 | + all_fields = [field['name'] for field in ts.get_ticket_fields()] | |
232 | + for k, v in attributes.iteritems(): | |
233 | + if k in all_fields and k != 'status': | |
234 | + t[k] = v | |
235 | + # TicketModule reads req.args - need to move things there... | |
236 | + req.args.update(attributes) | |
237 | + req.args['comment'] = comment | |
238 | + req.args['ts'] = time_changed | |
239 | + changes, problems = tm.get_ticket_changes(req, t, action) | |
240 | + for warning in problems: | |
241 | + add_warning(req, "Rpc ticket.update: %s" % warning) | |
242 | + valid = problems and False or tm._validate_ticket(req, t) | |
243 | + if not valid: | |
244 | + raise TracError( | |
245 | + " ".join([warning for warning in req.chrome['warnings']])) | |
246 | + else: | |
247 | + tm._apply_ticket_changes(t, changes) | |
248 | + self.log.debug("Rpc ticket.update save: %s" % repr(t.values)) | |
249 | + t.save_changes(author, comment, when=when) | |
250 | + # Apply workflow side-effects | |
251 | + for controller in controllers: | |
252 | + controller.apply_action_side_effects(req, t, action) | |
253 | + # Call ticket change listeners | |
254 | + for listener in ts.change_listeners: | |
255 | + listener.ticket_changed(t, comment, author, t._old) | |
256 | + if notify: | |
257 | + try: | |
258 | + tn = TicketNotifyEmail(self.env) | |
259 | + tn.notify(t, newticket=False, modtime=when) | |
260 | + except Exception, e: | |
261 | + self.log.exception("Failure sending notification on change of " | |
262 | + "ticket #%s: %s" % (t.id, e)) | |
263 | + return self.get(req, t.id) | |
264 | + | |
265 | + def delete(self, req, id): | |
266 | + """ Delete ticket with the given id. """ | |
267 | + t = model.Ticket(self.env, id) | |
268 | + req.perm(t.resource).require('TICKET_ADMIN') | |
269 | + t.delete() | |
270 | + ts = TicketSystem(self.env) | |
271 | + # Call ticket change listeners | |
272 | + for listener in ts.change_listeners: | |
273 | + listener.ticket_deleted(t) | |
274 | + | |
275 | + def changeLog(self, req, id, when=0): | |
276 | + t = model.Ticket(self.env, id) | |
277 | + req.perm(t.resource).require('TICKET_VIEW') | |
278 | + for date, author, field, old, new, permanent in t.get_changelog(when): | |
279 | + yield (date, author, field, old, new, permanent) | |
280 | + # Use existing documentation from Ticket model | |
281 | + changeLog.__doc__ = inspect.getdoc(model.Ticket.get_changelog) | |
282 | + | |
283 | + def listAttachments(self, req, ticket): | |
284 | + """ Lists attachments for a given ticket. Returns (filename, | |
285 | + description, size, time, author) for each attachment.""" | |
286 | + attachments = [] | |
287 | + for a in Attachment.select(self.env, 'ticket', ticket): | |
288 | + if 'ATTACHMENT_VIEW' in req.perm(a.resource): | |
289 | + yield (a.filename, a.description, a.size, a.date, a.author) | |
290 | + | |
291 | + def getAttachment(self, req, ticket, filename): | |
292 | + """ returns the content of an attachment. """ | |
293 | + attachment = Attachment(self.env, 'ticket', ticket, filename) | |
294 | + req.perm(attachment.resource).require('ATTACHMENT_VIEW') | |
295 | + return Binary(attachment.open().read()) | |
296 | + | |
297 | + def putAttachment(self, req, ticket, filename, description, data, replace=True): | |
298 | + """ Add an attachment, optionally (and defaulting to) overwriting an | |
299 | + existing one. Returns filename.""" | |
300 | + if not model.Ticket(self.env, ticket).exists: | |
301 | + raise ResourceNotFound('Ticket "%s" does not exist' % ticket) | |
302 | + if replace: | |
303 | + try: | |
304 | + attachment = Attachment(self.env, 'ticket', ticket, filename) | |
305 | + req.perm(attachment.resource).require('ATTACHMENT_DELETE') | |
306 | + attachment.delete() | |
307 | + except TracError: | |
308 | + pass | |
309 | + attachment = Attachment(self.env, 'ticket', ticket) | |
310 | + req.perm(attachment.resource).require('ATTACHMENT_CREATE') | |
311 | + attachment.author = req.authname | |
312 | + attachment.description = description | |
313 | + attachment.insert(filename, StringIO(data.data), len(data.data)) | |
314 | + return attachment.filename | |
315 | + | |
316 | + def deleteAttachment(self, req, ticket, filename): | |
317 | + """ Delete an attachment. """ | |
318 | + if not model.Ticket(self.env, ticket).exists: | |
319 | + raise ResourceNotFound('Ticket "%s" does not exists' % ticket) | |
320 | + attachment = Attachment(self.env, 'ticket', ticket, filename) | |
321 | + req.perm(attachment.resource).require('ATTACHMENT_DELETE') | |
322 | + attachment.delete() | |
323 | + return True | |
324 | + | |
325 | + def getTicketFields(self, req): | |
326 | + """ Return a list of all ticket fields fields. """ | |
327 | + return TicketSystem(self.env).get_ticket_fields() | |
328 | + | |
329 | +class StatusRPC(Component): | |
330 | + """ An interface to Trac ticket status objects. | |
331 | + Note: Status is defined by workflow, and all methods except `getAll()` | |
332 | + are deprecated no-op methods - these will be removed later. """ | |
333 | + | |
334 | + implements(IXMLRPCHandler) | |
335 | + | |
336 | + # IXMLRPCHandler methods | |
337 | + def xmlrpc_namespace(self): | |
338 | + return 'ticket.status' | |
339 | + | |
340 | + def xmlrpc_methods(self): | |
341 | + yield ('TICKET_VIEW', ((list,),), self.getAll) | |
342 | + yield ('TICKET_VIEW', ((dict, str),), self.get) | |
343 | + yield ('TICKET_ADMIN', ((None, str,),), self.delete) | |
344 | + yield ('TICKET_ADMIN', ((None, str, dict),), self.create) | |
345 | + yield ('TICKET_ADMIN', ((None, str, dict),), self.update) | |
346 | + | |
347 | + def getAll(self, req): | |
348 | + """ Returns all ticket states described by active workflow. """ | |
349 | + return TicketSystem(self.env).get_all_status() | |
350 | + | |
351 | + def get(self, req, name): | |
352 | + """ Deprecated no-op method. Do not use. """ | |
353 | + # FIXME: Remove | |
354 | + return '0' | |
355 | + | |
356 | + def delete(self, req, name): | |
357 | + """ Deprecated no-op method. Do not use. """ | |
358 | + # FIXME: Remove | |
359 | + return 0 | |
360 | + | |
361 | + def create(self, req, name, attributes): | |
362 | + """ Deprecated no-op method. Do not use. """ | |
363 | + # FIXME: Remove | |
364 | + return 0 | |
365 | + | |
366 | + def update(self, req, name, attributes): | |
367 | + """ Deprecated no-op method. Do not use. """ | |
368 | + # FIXME: Remove | |
369 | + return 0 | |
370 | + | |
371 | +def ticketModelFactory(cls, cls_attributes): | |
372 | + """ Return a class which exports an interface to trac.ticket.model.<cls>. """ | |
373 | + class TicketModelImpl(Component): | |
374 | + implements(IXMLRPCHandler) | |
375 | + | |
376 | + def xmlrpc_namespace(self): | |
377 | + return 'ticket.' + cls.__name__.lower() | |
378 | + | |
379 | + def xmlrpc_methods(self): | |
380 | + yield ('TICKET_VIEW', ((list,),), self.getAll) | |
381 | + yield ('TICKET_VIEW', ((dict, str),), self.get) | |
382 | + yield ('TICKET_ADMIN', ((None, str,),), self.delete) | |
383 | + yield ('TICKET_ADMIN', ((None, str, dict),), self.create) | |
384 | + yield ('TICKET_ADMIN', ((None, str, dict),), self.update) | |
385 | + | |
386 | + def getAll(self, req): | |
387 | + for i in cls.select(self.env): | |
388 | + yield i.name | |
389 | + getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() | |
390 | + | |
391 | + def get(self, req, name): | |
392 | + i = cls(self.env, name) | |
393 | + attributes= {} | |
394 | + for k, default in cls_attributes.iteritems(): | |
395 | + v = getattr(i, k) | |
396 | + if v is None: | |
397 | + v = default | |
398 | + attributes[k] = v | |
399 | + return attributes | |
400 | + get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() | |
401 | + | |
402 | + def delete(self, req, name): | |
403 | + cls(self.env, name).delete() | |
404 | + delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() | |
405 | + | |
406 | + def create(self, req, name, attributes): | |
407 | + i = cls(self.env) | |
408 | + i.name = name | |
409 | + for k, v in attributes.iteritems(): | |
410 | + setattr(i, k, v) | |
411 | + i.insert(); | |
412 | + create.__doc__ = """ Create a new ticket %s with the given attributes. """ % cls.__name__.lower() | |
413 | + | |
414 | + def update(self, req, name, attributes): | |
415 | + self._updateHelper(name, attributes).update() | |
416 | + update.__doc__ = """ Update ticket %s with the given attributes. """ % cls.__name__.lower() | |
417 | + | |
418 | + def _updateHelper(self, name, attributes): | |
419 | + i = cls(self.env, name) | |
420 | + for k, v in attributes.iteritems(): | |
421 | + setattr(i, k, v) | |
422 | + return i | |
423 | + TicketModelImpl.__doc__ = """ Interface to ticket %s objects. """ % cls.__name__.lower() | |
424 | + TicketModelImpl.__name__ = '%sRPC' % cls.__name__ | |
425 | + return TicketModelImpl | |
426 | + | |
427 | +def ticketEnumFactory(cls): | |
428 | + """ Return a class which exports an interface to one of the Trac ticket abstract enum types. """ | |
429 | + class AbstractEnumImpl(Component): | |
430 | + implements(IXMLRPCHandler) | |
431 | + | |
432 | + def xmlrpc_namespace(self): | |
433 | + return 'ticket.' + cls.__name__.lower() | |
434 | + | |
435 | + def xmlrpc_methods(self): | |
436 | + yield ('TICKET_VIEW', ((list,),), self.getAll) | |
437 | + yield ('TICKET_VIEW', ((str, str),), self.get) | |
438 | + yield ('TICKET_ADMIN', ((None, str,),), self.delete) | |
439 | + yield ('TICKET_ADMIN', ((None, str, str),), self.create) | |
440 | + yield ('TICKET_ADMIN', ((None, str, str),), self.update) | |
441 | + | |
442 | + def getAll(self, req): | |
443 | + for i in cls.select(self.env): | |
444 | + yield i.name | |
445 | + getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() | |
446 | + | |
447 | + def get(self, req, name): | |
448 | + if (cls.__name__ == 'Status'): | |
449 | + i = cls(self.env) | |
450 | + x = name | |
451 | + else: | |
452 | + i = cls(self.env, name) | |
453 | + x = i.value | |
454 | + return x | |
455 | + get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() | |
456 | + | |
457 | + def delete(self, req, name): | |
458 | + cls(self.env, name).delete() | |
459 | + delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() | |
460 | + | |
461 | + def create(self, req, name, value): | |
462 | + i = cls(self.env) | |
463 | + i.name = name | |
464 | + i.value = value | |
465 | + i.insert() | |
466 | + create.__doc__ = """ Create a new ticket %s with the given value. """ % cls.__name__.lower() | |
467 | + | |
468 | + def update(self, req, name, value): | |
469 | + self._updateHelper(name, value).update() | |
470 | + update.__doc__ = """ Update ticket %s with the given value. """ % cls.__name__.lower() | |
471 | + | |
472 | + def _updateHelper(self, name, value): | |
473 | + i = cls(self.env, name) | |
474 | + i.value = value | |
475 | + return i | |
476 | + | |
477 | + AbstractEnumImpl.__doc__ = """ Interface to ticket %s. """ % cls.__name__.lower() | |
478 | + AbstractEnumImpl.__name__ = '%sRPC' % cls.__name__ | |
479 | + return AbstractEnumImpl | |
480 | + | |
481 | +ticketModelFactory(model.Component, {'name': '', 'owner': '', 'description': ''}) | |
482 | +ticketModelFactory(model.Version, {'name': '', 'time': 0, 'description': ''}) | |
483 | +ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) | |
484 | + | |
485 | +ticketEnumFactory(model.Type) | |
486 | +ticketEnumFactory(model.Resolution) | |
487 | +ticketEnumFactory(model.Priority) | |
488 | +ticketEnumFactory(model.Severity) |
@@ -0,0 +1,57 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +from trac.util.compat import any | |
10 | + | |
11 | +try: | |
12 | + from cStringIO import StringIO | |
13 | +except ImportError: | |
14 | + from StringIO import StringIO | |
15 | + | |
16 | +try: | |
17 | + # Method only available in Trac 0.11.3 or higher | |
18 | + from trac.util.text import exception_to_unicode | |
19 | +except ImportError: | |
20 | + def exception_to_unicode(e, traceback=""): | |
21 | + from trac.util.text import to_unicode | |
22 | + message = '%s: %s' % (e.__class__.__name__, to_unicode(e)) | |
23 | + if traceback: | |
24 | + from trac.util import get_last_traceback | |
25 | + traceback_only = get_last_traceback().split('\n')[:-2] | |
26 | + message = '\n%s\n%s' % (to_unicode('\n'.join(traceback_only)), | |
27 | + message) | |
28 | + return message | |
29 | + | |
30 | +try: | |
31 | + # Constant available from Trac 0.12dev r8612 | |
32 | + from trac.util.text import empty | |
33 | +except ImportError: | |
34 | + empty = None | |
35 | + | |
36 | +def accepts_mimetype(req, mimetype): | |
37 | + if isinstance(mimetype, basestring): | |
38 | + mimetype = (mimetype,) | |
39 | + accept = req.get_header('Accept') | |
40 | + if accept is None : | |
41 | + # Don't make judgements if no MIME type expected and method is GET | |
42 | + return req.method == 'GET' | |
43 | + else : | |
44 | + accept = accept.split(',') | |
45 | + return any(x.strip().startswith(y) for x in accept for y in mimetype) | |
46 | + | |
47 | +def prepare_docs(text, indent=4): | |
48 | + r"""Remove leading whitespace""" | |
49 | + return ''.join(l[indent:] for l in text.splitlines(True)) | |
50 | + | |
51 | +try: | |
52 | + # Micro-second support added to 0.12dev r9210 | |
53 | + from trac.util.datefmt import to_utimestamp, from_utimestamp | |
54 | +except ImportError: | |
55 | + from trac.util.datefmt import to_timestamp, to_datetime, utc | |
56 | + to_utimestamp = to_timestamp | |
57 | + from_utimestamp = lambda x: to_datetime(x, utc) |
@@ -0,0 +1,208 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +import sys | |
10 | +from types import GeneratorType | |
11 | + | |
12 | +from pkg_resources import resource_filename | |
13 | + | |
14 | +from genshi.builder import tag | |
15 | +from genshi.template.base import TemplateSyntaxError, BadDirectiveError | |
16 | +from genshi.template.text import TextTemplate | |
17 | + | |
18 | +from trac.core import * | |
19 | +from trac.perm import PermissionError | |
20 | +from trac.resource import ResourceNotFound | |
21 | +from trac.util.text import to_unicode | |
22 | +from trac.util.translation import _ | |
23 | +from trac.web.api import RequestDone, HTTPUnsupportedMediaType, \ | |
24 | + HTTPInternalError | |
25 | +from trac.web.main import IRequestHandler | |
26 | +from trac.web.chrome import ITemplateProvider, INavigationContributor, \ | |
27 | + add_stylesheet, add_script, add_ctxtnav | |
28 | +from trac.wiki.formatter import wiki_to_oneliner | |
29 | + | |
30 | +from tracrpc.api import XMLRPCSystem, IRPCProtocol, ProtocolException, \ | |
31 | + RPCError, ServiceException | |
32 | +from tracrpc.util import accepts_mimetype | |
33 | + | |
34 | +__all__ = ['RPCWeb'] | |
35 | + | |
36 | +class RPCWeb(Component): | |
37 | + """ Handle RPC calls from HTTP clients, as well as presenting a list of | |
38 | + methods available to the currently logged in user. Browsing to | |
39 | + <trac>/rpc or <trac>/login/rpc will display this list. """ | |
40 | + | |
41 | + implements(IRequestHandler, ITemplateProvider, INavigationContributor) | |
42 | + | |
43 | + protocols = ExtensionPoint(IRPCProtocol) | |
44 | + | |
45 | + # IRequestHandler methods | |
46 | + | |
47 | + def match_request(self, req): | |
48 | + """ Look for available protocols serving at requested path and | |
49 | + content-type. """ | |
50 | + content_type = req.get_header('Content-Type') or 'text/html' | |
51 | + must_handle_request = req.path_info in ('/rpc', '/login/rpc') | |
52 | + for protocol in self.protocols: | |
53 | + for p_path, p_type in protocol.rpc_match(): | |
54 | + if req.path_info in ['/%s' % p_path, '/login/%s' % p_path]: | |
55 | + must_handle_request = True | |
56 | + if content_type.startswith(p_type): | |
57 | + req.args['protocol'] = protocol | |
58 | + return True | |
59 | + # No protocol call, need to handle for docs or error if handled path | |
60 | + return must_handle_request | |
61 | + | |
62 | + def process_request(self, req): | |
63 | + protocol = req.args.get('protocol', None) | |
64 | + content_type = req.get_header('Content-Type') or 'text/html' | |
65 | + if protocol: | |
66 | + # Perform the method call | |
67 | + self.log.debug("RPC incoming request of content type '%s' " \ | |
68 | + "dispatched to %s" % (content_type, repr(protocol))) | |
69 | + self._rpc_process(req, protocol, content_type) | |
70 | + elif accepts_mimetype(req, 'text/html') \ | |
71 | + or content_type.startswith('text/html'): | |
72 | + return self._dump_docs(req) | |
73 | + else: | |
74 | + # Attempt at API call gone wrong. Raise a plain-text 415 error | |
75 | + body = "No protocol matching Content-Type '%s' at path '%s'." % ( | |
76 | + content_type, req.path_info) | |
77 | + self.log.error(body) | |
78 | + req.send_error(None, template='', content_type='text/plain', | |
79 | + status=HTTPUnsupportedMediaType.code, env=None, data=body) | |
80 | + | |
81 | + # Internal methods | |
82 | + | |
83 | + def _dump_docs(self, req): | |
84 | + self.log.debug("Rendering docs") | |
85 | + | |
86 | + # Dump RPC documentation | |
87 | + req.perm.require('XML_RPC') # Need at least XML_RPC | |
88 | + namespaces = {} | |
89 | + for method in XMLRPCSystem(self.env).all_methods(req): | |
90 | + namespace = method.namespace.replace('.', '_') | |
91 | + if namespace not in namespaces: | |
92 | + namespaces[namespace] = { | |
93 | + 'description' : wiki_to_oneliner( | |
94 | + method.namespace_description, | |
95 | + self.env, req=req), | |
96 | + 'methods' : [], | |
97 | + 'namespace' : method.namespace, | |
98 | + } | |
99 | + try: | |
100 | + namespaces[namespace]['methods'].append( | |
101 | + (method.signature, | |
102 | + wiki_to_oneliner( | |
103 | + method.description, self.env, req=req), | |
104 | + method.permission)) | |
105 | + except Exception, e: | |
106 | + from tracrpc.util import StringIO | |
107 | + import traceback | |
108 | + out = StringIO() | |
109 | + traceback.print_exc(file=out) | |
110 | + raise Exception('%s: %s\n%s' % (method.name, | |
111 | + str(e), out.getvalue())) | |
112 | + add_stylesheet(req, 'common/css/wiki.css') | |
113 | + add_stylesheet(req, 'tracrpc/rpc.css') | |
114 | + add_script(req, 'tracrpc/rpc.js') | |
115 | + return ('rpc.html', | |
116 | + {'rpc': {'functions': namespaces, | |
117 | + 'protocols': [p.rpc_info() + (list(p.rpc_match()),) \ | |
118 | + for p in self.protocols], | |
119 | + 'version': __import__('tracrpc', ['__version__']).__version__ | |
120 | + }, | |
121 | + 'expand_docs': self._expand_docs | |
122 | + }, | |
123 | + None) | |
124 | + | |
125 | + def _expand_docs(self, docs, ctx): | |
126 | + try : | |
127 | + tmpl = TextTemplate(docs) | |
128 | + return tmpl.generate(**dict(ctx.items())).render() | |
129 | + except (TemplateSyntaxError, BadDirectiveError), exc: | |
130 | + self.log.exception("Syntax error rendering protocol documentation") | |
131 | + return "'''Syntax error:''' [[BR]] %s" % (str(exc),) | |
132 | + except Exception: | |
133 | + self.log.exception("Runtime error rendering protocol documentation") | |
134 | + return "Error rendering protocol documentation. " \ | |
135 | + "Contact your '''Trac''' administrator for details" | |
136 | + | |
137 | + def _rpc_process(self, req, protocol, content_type): | |
138 | + """Process incoming RPC request and finalize response.""" | |
139 | + proto_id = protocol.rpc_info()[0] | |
140 | + rpcreq = req.rpc = {'mimetype': content_type} | |
141 | + try : | |
142 | + self.log.debug("RPC(%s) call by '%s'", proto_id, req.authname) | |
143 | + rpcreq = req.rpc = protocol.parse_rpc_request(req, content_type) | |
144 | + rpcreq['mimetype'] = content_type | |
145 | + | |
146 | + # Important ! Check after parsing RPC request to add | |
147 | + # protocol-specific fields in response | |
148 | + # (e.g. JSON-RPC response `id`) | |
149 | + req.perm.require('XML_RPC') # Need at least XML_RPC | |
150 | + | |
151 | + method_name = rpcreq.get('method') | |
152 | + if method_name is None : | |
153 | + raise ProtocolException('Missing method name') | |
154 | + args = rpcreq.get('params') or [] | |
155 | + self.log.debug("RPC(%s) call by '%s' %s", proto_id, \ | |
156 | + req.authname, method_name) | |
157 | + try : | |
158 | + result = (XMLRPCSystem(self.env).get_method(method_name)(req, args))[0] | |
159 | + if isinstance(result, GeneratorType): | |
160 | + result = list(result) | |
161 | + except (RPCError, PermissionError, ResourceNotFound), e: | |
162 | + raise | |
163 | + except Exception: | |
164 | + e, tb = sys.exc_info()[-2:] | |
165 | + raise ServiceException(e), None, tb | |
166 | + else : | |
167 | + protocol.send_rpc_result(req, result) | |
168 | + except RequestDone : | |
169 | + raise | |
170 | + except (RPCError, PermissionError, ResourceNotFound), e: | |
171 | + self.log.exception("RPC(%s) Error", proto_id) | |
172 | + try : | |
173 | + protocol.send_rpc_error(req, e) | |
174 | + except RequestDone : | |
175 | + raise | |
176 | + except Exception, e : | |
177 | + self.log.exception("RPC(%s) Unhandled protocol error", proto_id) | |
178 | + self._send_unknown_error(req, e) | |
179 | + except Exception, e : | |
180 | + self.log.exception("RPC(%s) Unhandled protocol error", proto_id) | |
181 | + self._send_unknown_error(req, e) | |
182 | + | |
183 | + def _send_unknown_error(self, req, e): | |
184 | + """Last recourse if protocol cannot handle the RPC request | error""" | |
185 | + method_name = req.rpc and req.rpc.get('method') or '(undefined)' | |
186 | + body = "Unhandled protocol error calling '%s': %s" % ( | |
187 | + method_name, to_unicode(e)) | |
188 | + req.send_error(None, template='', content_type='text/plain', | |
189 | + env=None, data=body, status=HTTPInternalError.code) | |
190 | + | |
191 | + # ITemplateProvider methods | |
192 | + | |
193 | + def get_htdocs_dirs(self): | |
194 | + yield ('tracrpc', resource_filename(__name__, 'htdocs')) | |
195 | + | |
196 | + def get_templates_dirs(self): | |
197 | + yield resource_filename(__name__, 'templates') | |
198 | + | |
199 | + # INavigationContributor methods | |
200 | + | |
201 | + def get_active_navigation_item(self, req): | |
202 | + pass | |
203 | + | |
204 | + def get_navigation_items(self, req): | |
205 | + if req.perm.has_permission('XML_RPC'): | |
206 | + yield ('metanav', 'rpc', | |
207 | + tag.a('API', href=req.href.rpc(), accesskey=1)) | |
208 | + |
@@ -0,0 +1,209 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +import os | |
10 | +from datetime import datetime | |
11 | + | |
12 | +from trac.attachment import Attachment | |
13 | +from trac.core import * | |
14 | +from trac.mimeview import Context | |
15 | +from trac.resource import Resource, ResourceNotFound | |
16 | +from trac.wiki.api import WikiSystem | |
17 | +from trac.wiki.model import WikiPage | |
18 | +from trac.wiki.formatter import wiki_to_html, format_to_html | |
19 | + | |
20 | +from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary | |
21 | +from tracrpc.util import StringIO, to_utimestamp, from_utimestamp | |
22 | + | |
23 | +__all__ = ['WikiRPC'] | |
24 | + | |
25 | +class WikiRPC(Component): | |
26 | + """Superset of the | |
27 | + [http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2 WikiRPC API]. """ | |
28 | + | |
29 | + implements(IXMLRPCHandler) | |
30 | + | |
31 | + def __init__(self): | |
32 | + self.wiki = WikiSystem(self.env) | |
33 | + | |
34 | + def xmlrpc_namespace(self): | |
35 | + return 'wiki' | |
36 | + | |
37 | + def xmlrpc_methods(self): | |
38 | + yield (None, ((dict, datetime),), self.getRecentChanges) | |
39 | + yield ('WIKI_VIEW', ((int,),), self.getRPCVersionSupported) | |
40 | + yield (None, ((str, str), (str, str, int),), self.getPage) | |
41 | + yield (None, ((str, str, int),), self.getPage, 'getPageVersion') | |
42 | + yield (None, ((str, str), (str, str, int)), self.getPageHTML) | |
43 | + yield (None, ((str, str), (str, str, int)), self.getPageHTML, 'getPageHTMLVersion') | |
44 | + yield (None, ((list,),), self.getAllPages) | |
45 | + yield (None, ((dict, str), (dict, str, int)), self.getPageInfo) | |
46 | + yield (None, ((dict, str, int),), self.getPageInfo, 'getPageInfoVersion') | |
47 | + yield (None, ((bool, str, str, dict),), self.putPage) | |
48 | + yield (None, ((list, str),), self.listAttachments) | |
49 | + yield (None, ((Binary, str),), self.getAttachment) | |
50 | + yield (None, ((bool, str, Binary),), self.putAttachment) | |
51 | + yield (None, ((bool, str, str, str, Binary), | |
52 | + (bool, str, str, str, Binary, bool)), | |
53 | + self.putAttachmentEx) | |
54 | + yield (None, ((bool, str),(bool, str, int)), self.deletePage) | |
55 | + yield (None, ((bool, str),), self.deleteAttachment) | |
56 | + yield ('WIKI_VIEW', ((list, str),), self.listLinks) | |
57 | + yield ('WIKI_VIEW', ((str, str),), self.wikiToHtml) | |
58 | + | |
59 | + def _page_info(self, name, when, author, version, comment): | |
60 | + return dict(name=name, lastModified=when, | |
61 | + author=author, version=int(version), comment=comment) | |
62 | + | |
63 | + def getRecentChanges(self, req, since): | |
64 | + """ Get list of changed pages since timestamp """ | |
65 | + since = to_utimestamp(since) | |
66 | + wiki_realm = Resource('wiki') | |
67 | + db = self.env.get_db_cnx() | |
68 | + cursor = db.cursor() | |
69 | + cursor.execute('SELECT name, max(time), author, version, comment FROM wiki' | |
70 | + ' WHERE time >= %s GROUP BY name ORDER BY max(time) DESC', (since,)) | |
71 | + result = [] | |
72 | + for name, when, author, version, comment in cursor: | |
73 | + if 'WIKI_VIEW' in req.perm(wiki_realm(id=name, version=version)): | |
74 | + result.append( | |
75 | + self._page_info(name, from_utimestamp(when), | |
76 | + author, version, comment)) | |
77 | + return result | |
78 | + | |
79 | + def getRPCVersionSupported(self, req): | |
80 | + """ Returns 2 with this version of the Trac API. """ | |
81 | + return 2 | |
82 | + | |
83 | + def getPage(self, req, pagename, version=None): | |
84 | + """ Get the raw Wiki text of page, latest version. """ | |
85 | + page = WikiPage(self.env, pagename, version) | |
86 | + req.perm(page.resource).require('WIKI_VIEW') | |
87 | + if page.exists: | |
88 | + return page.text | |
89 | + else: | |
90 | + msg = 'Wiki page "%s" does not exist' % pagename | |
91 | + if version is not None: | |
92 | + msg += ' at version %s' % version | |
93 | + raise ResourceNotFound(msg) | |
94 | + | |
95 | + def getPageHTML(self, req, pagename, version=None): | |
96 | + """ Return page in rendered HTML, latest version. """ | |
97 | + text = self.getPage(req, pagename, version) | |
98 | + resource = Resource('wiki', pagename, version) | |
99 | + context = Context.from_request(req, resource, absurls=True) | |
100 | + html = format_to_html(self.env, context, text) | |
101 | + return '<html><body>%s</body></html>' % html | |
102 | + | |
103 | + def getAllPages(self, req): | |
104 | + """ Returns a list of all pages. The result is an array of utf8 pagenames. """ | |
105 | + pages = [] | |
106 | + for page in self.wiki.get_pages(): | |
107 | + if 'WIKI_VIEW' in req.perm(Resource('wiki', page)): | |
108 | + pages.append(page) | |
109 | + return pages | |
110 | + | |
111 | + def getPageInfo(self, req, pagename, version=None): | |
112 | + """ Returns information about the given page. """ | |
113 | + page = WikiPage(self.env, pagename, version) | |
114 | + req.perm(page.resource).require('WIKI_VIEW') | |
115 | + if page.exists: | |
116 | + last_update = page.get_history().next() | |
117 | + return self._page_info(page.name, last_update[1], | |
118 | + last_update[2], page.version, page.comment) | |
119 | + | |
120 | + def putPage(self, req, pagename, content, attributes): | |
121 | + """ writes the content of the page. """ | |
122 | + page = WikiPage(self.env, pagename) | |
123 | + if page.readonly: | |
124 | + req.perm(page.resource).require('WIKI_ADMIN') | |
125 | + elif not page.exists: | |
126 | + req.perm(page.resource).require('WIKI_CREATE') | |
127 | + else: | |
128 | + req.perm(page.resource).require('WIKI_MODIFY') | |
129 | + | |
130 | + page.text = content | |
131 | + if req.perm(page.resource).has_permission('WIKI_ADMIN'): | |
132 | + page.readonly = attributes.get('readonly') and 1 or 0 | |
133 | + | |
134 | + page.save(attributes.get('author', req.authname), | |
135 | + attributes.get('comment'), req.remote_addr) | |
136 | + return True | |
137 | + | |
138 | + def deletePage(self, req, name, version=None): | |
139 | + """Delete a Wiki page (all versions) or a specific version by | |
140 | + including an optional version number. Attachments will also be | |
141 | + deleted if page no longer exists. Returns True for success.""" | |
142 | + wp = WikiPage(self.env, name, version) | |
143 | + req.perm(wp.resource).require('WIKI_DELETE') | |
144 | + try: | |
145 | + wp.delete(version) | |
146 | + return True | |
147 | + except: | |
148 | + return False | |
149 | + | |
150 | + def listAttachments(self, req, pagename): | |
151 | + """ Lists attachments on a given page. """ | |
152 | + for a in Attachment.select(self.env, 'wiki', pagename): | |
153 | + if 'ATTACHMENT_VIEW' in req.perm(a.resource): | |
154 | + yield pagename + '/' + a.filename | |
155 | + | |
156 | + def getAttachment(self, req, path): | |
157 | + """ returns the content of an attachment. """ | |
158 | + pagename, filename = os.path.split(path) | |
159 | + attachment = Attachment(self.env, 'wiki', pagename, filename) | |
160 | + req.perm(attachment.resource).require('ATTACHMENT_VIEW') | |
161 | + return Binary(attachment.open().read()) | |
162 | + | |
163 | + def putAttachment(self, req, path, data): | |
164 | + """ (over)writes an attachment. Returns True if successful. | |
165 | + | |
166 | + This method is compatible with WikiRPC. `putAttachmentEx` has a more | |
167 | + extensive set of (Trac-specific) features. """ | |
168 | + pagename, filename = os.path.split(path) | |
169 | + self.putAttachmentEx(req, pagename, filename, None, data) | |
170 | + return True | |
171 | + | |
172 | + def putAttachmentEx(self, req, pagename, filename, description, data, replace=True): | |
173 | + """ Attach a file to a Wiki page. Returns the (possibly transformed) | |
174 | + filename of the attachment. | |
175 | + | |
176 | + Use this method if you don't care about WikiRPC compatibility. """ | |
177 | + if not WikiPage(self.env, pagename).exists: | |
178 | + raise ResourceNotFound, 'Wiki page "%s" does not exist' % pagename | |
179 | + if replace: | |
180 | + try: | |
181 | + attachment = Attachment(self.env, 'wiki', pagename, filename) | |
182 | + req.perm(attachment.resource).require('ATTACHMENT_DELETE') | |
183 | + attachment.delete() | |
184 | + except TracError: | |
185 | + pass | |
186 | + attachment = Attachment(self.env, 'wiki', pagename) | |
187 | + req.perm(attachment.resource).require('ATTACHMENT_CREATE') | |
188 | + attachment.author = req.authname | |
189 | + attachment.description = description | |
190 | + attachment.insert(filename, StringIO(data.data), len(data.data)) | |
191 | + return attachment.filename | |
192 | + | |
193 | + def deleteAttachment(self, req, path): | |
194 | + """ Delete an attachment. """ | |
195 | + pagename, filename = os.path.split(path) | |
196 | + if not WikiPage(self.env, pagename).exists: | |
197 | + raise ResourceNotFound, 'Wiki page "%s" does not exist' % pagename | |
198 | + attachment = Attachment(self.env, 'wiki', pagename, filename) | |
199 | + req.perm(attachment.resource).require('ATTACHMENT_DELETE') | |
200 | + attachment.delete() | |
201 | + return True | |
202 | + | |
203 | + def listLinks(self, req, pagename): | |
204 | + """ ''Not implemented'' """ | |
205 | + return [] | |
206 | + | |
207 | + def wikiToHtml(self, req, text): | |
208 | + """ Render arbitrary Wiki text as HTML. """ | |
209 | + return unicode(wiki_to_html(text, self.env, req, absurls=1)) |
@@ -0,0 +1,205 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) | |
6 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
7 | +""" | |
8 | + | |
9 | +import datetime | |
10 | +import time | |
11 | +import xmlrpclib | |
12 | + | |
13 | +import genshi | |
14 | + | |
15 | +from trac.core import * | |
16 | +from trac.perm import PermissionError | |
17 | +from trac.resource import ResourceNotFound | |
18 | +from trac.util.datefmt import utc | |
19 | +from trac.util.text import to_unicode | |
20 | +from trac.web.api import RequestDone | |
21 | + | |
22 | +from tracrpc.api import XMLRPCSystem, IRPCProtocol, Binary, \ | |
23 | + RPCError, MethodNotFound, ProtocolException, ServiceException | |
24 | +from tracrpc.util import empty, prepare_docs | |
25 | + | |
26 | +__all__ = ['XmlRpcProtocol'] | |
27 | + | |
28 | +def to_xmlrpc_datetime(dt): | |
29 | + """ Convert a datetime.datetime object to a xmlrpclib DateTime object """ | |
30 | + return xmlrpclib.DateTime(dt.utctimetuple()) | |
31 | + | |
32 | +def from_xmlrpc_datetime(data): | |
33 | + """Return datetime (in utc) from XMLRPC datetime string (is always utc)""" | |
34 | + t = list(time.strptime(data.value, "%Y%m%dT%H:%M:%S")[0:6]) | |
35 | + return apply(datetime.datetime, t, {'tzinfo': utc}) | |
36 | + | |
37 | +class XmlRpcProtocol(Component): | |
38 | + r""" | |
39 | + There should be XML-RPC client implementations available for all | |
40 | + popular programming languages. | |
41 | + Example call using `curl`: | |
42 | + | |
43 | + {{{ | |
44 | + user: ~ > cat body.xml | |
45 | + <?xml version="1.0"?> | |
46 | + <methodCall> | |
47 | + <methodName>wiki.getPage</methodName> | |
48 | + <params> | |
49 | + <param><string>WikiStart</string></param> | |
50 | + </params> | |
51 | + </methodCall> | |
52 | + | |
53 | + user: ~ > curl -H "Content-Type: application/xml" --data @body.xml ${req.abs_href.rpc()} | |
54 | + <?xml version='1.0'?> | |
55 | + <methodResponse> | |
56 | + <params> | |
57 | + <param> | |
58 | + <value><string>= Welcome to.... | |
59 | + }}} | |
60 | + | |
61 | + The following snippet illustrates how to perform authenticated calls in Python. | |
62 | + | |
63 | + {{{ | |
64 | + >>> from xmlrpclib import ServerProxy | |
65 | + >>> p = ServerProxy('${req.abs_href.login('rpc').replace('://', '://%s:your_password@' % authname)}') | |
66 | + >>> p.system.getAPIVersion() | |
67 | + [${', '.join(rpc.version.split('.'))}] | |
68 | + }}} | |
69 | + """ | |
70 | + | |
71 | + implements(IRPCProtocol) | |
72 | + | |
73 | + # IRPCProtocol methods | |
74 | + | |
75 | + def rpc_info(self): | |
76 | + return ('XML-RPC', prepare_docs(self.__doc__)) | |
77 | + | |
78 | + def rpc_match(self): | |
79 | + # Legacy path xmlrpc provided for backwards compatibility: | |
80 | + # Using this order to get better docs | |
81 | + yield ('rpc', 'application/xml') | |
82 | + yield ('xmlrpc', 'application/xml') | |
83 | + yield ('rpc', 'text/xml') | |
84 | + yield ('xmlrpc', 'text/xml') | |
85 | + | |
86 | + def parse_rpc_request(self, req, content_type): | |
87 | + """ Parse XML-RPC requests.""" | |
88 | + try: | |
89 | + args, method = xmlrpclib.loads( | |
90 | + req.read(int(req.get_header('Content-Length')))) | |
91 | + except Exception, e: | |
92 | + self.log.debug("RPC(xml) parse error: %s", to_unicode(e)) | |
93 | + raise ProtocolException(xmlrpclib.Fault(-32700, to_unicode(e))) | |
94 | + else : | |
95 | + self.log.debug("RPC(xml) call by '%s', method '%s' with args: %s" \ | |
96 | + % (req.authname, method, repr(args))) | |
97 | + args = self._normalize_xml_input(args) | |
98 | + return {'method' : method, 'params' : args} | |
99 | + | |
100 | + def send_rpc_result(self, req, result): | |
101 | + """Send the result of the XML-RPC call back to the client.""" | |
102 | + rpcreq = req.rpc | |
103 | + method = rpcreq.get('method') | |
104 | + self.log.debug("RPC(xml) '%s' result: %s" % ( | |
105 | + method, repr(result))) | |
106 | + result = tuple(self._normalize_xml_output([result])) | |
107 | + self._send_response(req, | |
108 | + xmlrpclib.dumps(result, methodresponse=True), rpcreq['mimetype']) | |
109 | + | |
110 | + def send_rpc_error(self, req, e): | |
111 | + """Send an XML-RPC fault message back to the caller""" | |
112 | + rpcreq = req.rpc | |
113 | + fault = None | |
114 | + if isinstance(e, ProtocolException): | |
115 | + fault = e._exc | |
116 | + elif isinstance(e, ServiceException): | |
117 | + e = e._exc | |
118 | + elif isinstance(e, MethodNotFound): | |
119 | + fault = xmlrpclib.Fault(-32601, to_unicode(e)) | |
120 | + elif isinstance(e, PermissionError): | |
121 | + fault = xmlrpclib.Fault(403, to_unicode(e)) | |
122 | + elif isinstance(e, ResourceNotFound): | |
123 | + fault = xmlrpclib.Fault(404, to_unicode(e)) | |
124 | + | |
125 | + if fault is not None : | |
126 | + self._send_response(req, xmlrpclib.dumps(fault), rpcreq['mimetype']) | |
127 | + else : | |
128 | + self.log.error(e) | |
129 | + import traceback | |
130 | + from tracrpc.util import StringIO | |
131 | + out = StringIO() | |
132 | + traceback.print_exc(file = out) | |
133 | + self.log.error(out.getvalue()) | |
134 | + err_code = hasattr(e, 'code') and e.code or 1 | |
135 | + method = rpcreq.get('method') | |
136 | + self._send_response(req, | |
137 | + xmlrpclib.dumps( | |
138 | + xmlrpclib.Fault(err_code, | |
139 | + "'%s' while executing '%s()'" % (str(e), method)))) | |
140 | + | |
141 | + # Internal methods | |
142 | + | |
143 | + def _send_response(self, req, response, content_type='application/xml'): | |
144 | + response = to_unicode(response).encode("utf-8") | |
145 | + req.send_response(200) | |
146 | + req.send_header('Content-Type', content_type) | |
147 | + req.send_header('Content-Length', len(response)) | |
148 | + req.end_headers() | |
149 | + req.write(response) | |
150 | + raise RequestDone | |
151 | + | |
152 | + def _normalize_xml_input(self, args): | |
153 | + """ Normalizes arguments (at any level - traversing dicts and lists): | |
154 | + 1. xmlrpc.DateTime is converted to Python datetime | |
155 | + 2. tracrpc.api.Binary => xmlrpclib.Binary | |
156 | + 2. String line-endings same as from web (`\n` => `\r\n`) | |
157 | + """ | |
158 | + new_args = [] | |
159 | + for arg in args: | |
160 | + # self.env.log.debug("arg %s, type %s" % (arg, type(arg))) | |
161 | + if isinstance(arg, xmlrpclib.DateTime): | |
162 | + new_args.append(from_xmlrpc_datetime(arg)) | |
163 | + elif isinstance(arg, xmlrpclib.Binary): | |
164 | + arg.__class__ = Binary | |
165 | + new_args.append(arg) | |
166 | + elif isinstance(arg, basestring): | |
167 | + new_args.append(arg.replace("\n", "\r\n")) | |
168 | + elif isinstance(arg, dict): | |
169 | + for key, val in arg.items(): | |
170 | + arg[key], = self._normalize_xml_input([val]) | |
171 | + new_args.append(arg) | |
172 | + elif isinstance(arg, (list, tuple)): | |
173 | + new_args.append(self._normalize_xml_input(arg)) | |
174 | + else: | |
175 | + new_args.append(arg) | |
176 | + return new_args | |
177 | + | |
178 | + def _normalize_xml_output(self, result): | |
179 | + """ Normalizes and converts output (traversing it): | |
180 | + 1. None => '' | |
181 | + 2. datetime => xmlrpclib.DateTime | |
182 | + 3. Binary => xmlrpclib.Binary | |
183 | + 4. genshi.builder.Fragment|genshi.core.Markup => unicode | |
184 | + """ | |
185 | + new_result = [] | |
186 | + for res in result: | |
187 | + if isinstance(res, datetime.datetime): | |
188 | + new_result.append(to_xmlrpc_datetime(res)) | |
189 | + elif isinstance(res, Binary): | |
190 | + res.__class__ = xmlrpclib.Binary | |
191 | + new_result.append(res) | |
192 | + elif res is None or res is empty: | |
193 | + new_result.append('') | |
194 | + elif isinstance(res, (genshi.builder.Fragment, \ | |
195 | + genshi.core.Markup)): | |
196 | + new_result.append(to_unicode(res)) | |
197 | + elif isinstance(res, dict): | |
198 | + for key, val in res.items(): | |
199 | + res[key], = self._normalize_xml_output([val]) | |
200 | + new_result.append(res) | |
201 | + elif isinstance(res, list) or isinstance(res, tuple): | |
202 | + new_result.append(self._normalize_xml_output(res)) | |
203 | + else: | |
204 | + new_result.append(res) | |
205 | + return new_result |
@@ -0,0 +1,17 @@ | ||
1 | +K 25 | |
2 | +svn:wc:ra_dav:version-url | |
3 | +V 52 | |
4 | +/svn/!svn/ver/7970/xmlrpcplugin/trunk/tracrpc/htdocs | |
5 | +END | |
6 | +rpc.js | |
7 | +K 25 | |
8 | +svn:wc:ra_dav:version-url | |
9 | +V 59 | |
10 | +/svn/!svn/ver/7970/xmlrpcplugin/trunk/tracrpc/htdocs/rpc.js | |
11 | +END | |
12 | +rpc.css | |
13 | +K 25 | |
14 | +svn:wc:ra_dav:version-url | |
15 | +V 60 | |
16 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/tracrpc/htdocs/rpc.css | |
17 | +END |
@@ -0,0 +1,96 @@ | ||
1 | +10 | |
2 | + | |
3 | +dir | |
4 | +10247 | |
5 | +http://trac-hacks.org/svn/xmlrpcplugin/trunk/tracrpc/htdocs | |
6 | +http://trac-hacks.org/svn | |
7 | + | |
8 | + | |
9 | + | |
10 | +2010-05-21T10:25:18.560265Z | |
11 | +7970 | |
12 | +osimons | |
13 | + | |
14 | + | |
15 | + | |
16 | + | |
17 | + | |
18 | + | |
19 | + | |
20 | + | |
21 | + | |
22 | + | |
23 | + | |
24 | + | |
25 | + | |
26 | + | |
27 | +7322e99d-02ea-0310-aa39-e9a107903beb | |
28 | + | |
29 | +rpc.js | |
30 | +file | |
31 | + | |
32 | + | |
33 | + | |
34 | + | |
35 | +2010-05-21T10:25:18.560265Z | |
36 | +126f1d53406e4cdb11bfcba6e97ddd87 | |
37 | +2010-05-21T10:25:18.560265Z | |
38 | +7970 | |
39 | +osimons | |
40 | + | |
41 | + | |
42 | + | |
43 | + | |
44 | + | |
45 | + | |
46 | + | |
47 | + | |
48 | + | |
49 | + | |
50 | + | |
51 | + | |
52 | + | |
53 | + | |
54 | + | |
55 | + | |
56 | + | |
57 | + | |
58 | + | |
59 | + | |
60 | + | |
61 | +1096 | |
62 | + | |
63 | +rpc.css | |
64 | +file | |
65 | + | |
66 | + | |
67 | + | |
68 | + | |
69 | +2010-05-02T23:49:30.648005Z | |
70 | +d5914d2316007cf757b984f34109862c | |
71 | +2010-05-02T23:49:30.648005Z | |
72 | +7916 | |
73 | +osimons | |
74 | + | |
75 | + | |
76 | + | |
77 | + | |
78 | + | |
79 | + | |
80 | + | |
81 | + | |
82 | + | |
83 | + | |
84 | + | |
85 | + | |
86 | + | |
87 | + | |
88 | + | |
89 | + | |
90 | + | |
91 | + | |
92 | + | |
93 | + | |
94 | + | |
95 | +201 | |
96 | + |
@@ -0,0 +1,13 @@ | ||
1 | +#rpc-toc { | |
2 | + display: none; | |
3 | + float: right; | |
4 | + width: 15em; | |
5 | +} | |
6 | +#rpc-toc.wiki-toc h4 { | |
7 | + text-align: center; | |
8 | +} | |
9 | +#rpc-toc ul { | |
10 | + margin: 0; | |
11 | + padding: 0.3em 0.15em; | |
12 | + list-style-type: none; | |
13 | +} |
@@ -0,0 +1,28 @@ | ||
1 | +(function($) { | |
2 | + $(document).ready(function () { | |
3 | + // Create a Table of Contents (TOC) | |
4 | + $('#content .wikipage') | |
5 | + .prepend('<div id="rpc-toc" class="wiki-toc"><h4>Contents</h4><ul /></div>'); | |
6 | + function toc_entry(_this, item) { | |
7 | + return $('<li><a href="#' + _this.id + '" title="' | |
8 | + + $(item).text().replace(/^\s+|\s+$/g, '') | |
9 | + + '">' + _this.id.replace(/^rpc\./, '') + '</a></li>'); | |
10 | + } | |
11 | + var ul = $('#rpc-toc ul'); | |
12 | + $("#content").find("*[id]").each(function(index, item) { | |
13 | + var elem = undefined; | |
14 | + if (this.tagName == 'H2') { | |
15 | + elem = toc_entry(this, item); | |
16 | + elem.css('padding-top', '0.5em'); | |
17 | + } | |
18 | + if (this.tagName == 'H3') { | |
19 | + elem = toc_entry(this, item); | |
20 | + elem.css('padding-left', '1.2em'); | |
21 | + } | |
22 | + ul.append(elem); | |
23 | + }); | |
24 | + $('#rpc-toc').toggle(); | |
25 | + // Add anchors to headings | |
26 | + $("#content").find("h2,h3").addAnchor("Link here"); | |
27 | + }); | |
28 | +})(jQuery); |
@@ -0,0 +1,11 @@ | ||
1 | +K 25 | |
2 | +svn:wc:ra_dav:version-url | |
3 | +V 55 | |
4 | +/svn/!svn/ver/7970/xmlrpcplugin/trunk/tracrpc/templates | |
5 | +END | |
6 | +rpc.html | |
7 | +K 25 | |
8 | +svn:wc:ra_dav:version-url | |
9 | +V 64 | |
10 | +/svn/!svn/ver/7970/xmlrpcplugin/trunk/tracrpc/templates/rpc.html | |
11 | +END |
@@ -0,0 +1,62 @@ | ||
1 | +10 | |
2 | + | |
3 | +dir | |
4 | +10247 | |
5 | +http://trac-hacks.org/svn/xmlrpcplugin/trunk/tracrpc/templates | |
6 | +http://trac-hacks.org/svn | |
7 | + | |
8 | + | |
9 | + | |
10 | +2010-05-21T10:25:18.560265Z | |
11 | +7970 | |
12 | +osimons | |
13 | + | |
14 | + | |
15 | + | |
16 | + | |
17 | + | |
18 | + | |
19 | + | |
20 | + | |
21 | + | |
22 | + | |
23 | + | |
24 | + | |
25 | + | |
26 | + | |
27 | +7322e99d-02ea-0310-aa39-e9a107903beb | |
28 | + | |
29 | +rpc.html | |
30 | +file | |
31 | + | |
32 | + | |
33 | + | |
34 | + | |
35 | +2010-05-21T10:25:18.560265Z | |
36 | +17d2c0f77369d699fce59f6b557a0b59 | |
37 | +2010-05-21T10:25:18.560265Z | |
38 | +7970 | |
39 | +osimons | |
40 | + | |
41 | + | |
42 | + | |
43 | + | |
44 | + | |
45 | + | |
46 | + | |
47 | + | |
48 | + | |
49 | + | |
50 | + | |
51 | + | |
52 | + | |
53 | + | |
54 | + | |
55 | + | |
56 | + | |
57 | + | |
58 | + | |
59 | + | |
60 | + | |
61 | +3463 | |
62 | + |
@@ -0,0 +1,86 @@ | ||
1 | +<!DOCTYPE html | |
2 | + PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" | |
3 | + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> | |
4 | +<html xmlns="http://www.w3.org/1999/xhtml" | |
5 | + xmlns:py="http://genshi.edgewall.org/" | |
6 | + xmlns:xi="http://www.w3.org/2001/XInclude"> | |
7 | + <xi:include href="layout.html" /> | |
8 | + <xi:include href="macros.html" /> | |
9 | + <head> | |
10 | + <title>Remote Procedure Call (RPC)</title> | |
11 | + </head> | |
12 | + | |
13 | + <body> | |
14 | + <div id="content" class="wiki"> | |
15 | + <div class="wikipage searchable"> | |
16 | + <h1>Remote Procedure Call (RPC)</h1> | |
17 | + <p><strong>Installed API version :</strong> ${rpc.version} </p> | |
18 | + <h2 id="Protocols">Protocol reference:</h2> | |
19 | + <p>Below you will find a detailed description of all the RPC | |
20 | + protocols installed in this environment. This includes | |
21 | + supported content types as well as target URLs for | |
22 | + anonymous and authenticated access. Use this | |
23 | + information to interact with this environment from | |
24 | + a remote location. | |
25 | + </p> | |
26 | + <p>Libraries for remote procedure calls and parsing exists | |
27 | + for most major languages and platforms - use a tested, standard library | |
28 | + for consistent results.</p> | |
29 | + <dl py:for="protocol in rpc.protocols"> | |
30 | + <h3 id="${protocol[0]}">${protocol[0]}</h3> | |
31 | + <dd> | |
32 | + <p>For ${protocol[0]} protocol, use any one of:</p> | |
33 | + <ul> | |
34 | + <li py:for="ct, _ct_group in groupby(protocol[2], lambda (_, x) : x)"> | |
35 | + <tt>{'Content-Type': '$ct'}</tt> header with request to: | |
36 | + <py:with vars="ct_group = list(_ct_group)"> | |
37 | + <ul> | |
38 | + <li py:for="h, _ in ct_group"> | |
39 | + <a href="${req.abs_href(h)}">${req.abs_href(h)}</a> | |
40 | + for anonymous access | |
41 | + </li> | |
42 | + <li py:for="h, _ in ct_group"> | |
43 | + <a href="${req.abs_href.login(h)}">${req.abs_href.login(h)}</a> | |
44 | + for authenticated access | |
45 | + </li> | |
46 | + </ul> | |
47 | + </py:with> | |
48 | + </li> | |
49 | + </ul> | |
50 | + <div> | |
51 | + ${wiki_to_html(context, expand_docs(protocol[1], locals()['__data__']))} | |
52 | + </div> | |
53 | + </dd> | |
54 | + </dl> | |
55 | + | |
56 | + <h2 id="Methods">RPC exported functions</h2> | |
57 | + | |
58 | + <div id="searchable"> | |
59 | + <dl py:for="key in sorted(rpc.functions)" py:with="namespace = rpc.functions[key]"> | |
60 | + <h3 id="${'rpc.' + to_unicode(namespace.namespace)}"> | |
61 | + ${namespace.namespace} - ${namespace.description} | |
62 | + </h3> | |
63 | + <dd> | |
64 | + <table class="listing tickets"> | |
65 | + <thead> | |
66 | + <tr> | |
67 | + <th style="width:40%">Function</th> | |
68 | + <th style="width:45%">Description</th> | |
69 | + <th style="width:15%">Permission required</th> | |
70 | + </tr> | |
71 | + </thead> | |
72 | + <tbody py:for="idx, function in enumerate(namespace.methods)"> | |
73 | + <tr class="${'color3-' + (idx % 2 == 0 and 'even' or 'odd')}"> | |
74 | + <td style="padding-left:4em;text-indent:-4em">${function[0]}</td> | |
75 | + <td>${function[1]}</td> | |
76 | + <td>${function[2] or "By resource"}</td> | |
77 | + </tr> | |
78 | + </tbody> | |
79 | + </table> | |
80 | + </dd> | |
81 | + </dl> | |
82 | + </div> | |
83 | + </div> | |
84 | + </div> | |
85 | + </body> | |
86 | +</html> |
@@ -0,0 +1,47 @@ | ||
1 | +K 25 | |
2 | +svn:wc:ra_dav:version-url | |
3 | +V 51 | |
4 | +/svn/!svn/ver/9913/xmlrpcplugin/trunk/tracrpc/tests | |
5 | +END | |
6 | +api.py | |
7 | +K 25 | |
8 | +svn:wc:ra_dav:version-url | |
9 | +V 58 | |
10 | +/svn/!svn/ver/7916/xmlrpcplugin/trunk/tracrpc/tests/api.py | |
11 | +END | |
12 | +web_ui.py | |
13 | +K 25 | |
14 | +svn:wc:ra_dav:version-url | |
15 | +V 61 | |
16 | +/svn/!svn/ver/7971/xmlrpcplugin/trunk/tracrpc/tests/web_ui.py | |
17 | +END | |
18 | +json_rpc.py | |
19 | +K 25 | |
20 | +svn:wc:ra_dav:version-url | |
21 | +V 63 | |
22 | +/svn/!svn/ver/7966/xmlrpcplugin/trunk/tracrpc/tests/json_rpc.py | |
23 | +END | |
24 | +__init__.py | |
25 | +K 25 | |
26 | +svn:wc:ra_dav:version-url | |
27 | +V 63 | |
28 | +/svn/!svn/ver/7966/xmlrpcplugin/trunk/tracrpc/tests/__init__.py | |
29 | +END | |
30 | +ticket.py | |
31 | +K 25 | |
32 | +svn:wc:ra_dav:version-url | |
33 | +V 61 | |
34 | +/svn/!svn/ver/9913/xmlrpcplugin/trunk/tracrpc/tests/ticket.py | |
35 | +END | |
36 | +wiki.py | |
37 | +K 25 | |
38 | +svn:wc:ra_dav:version-url | |
39 | +V 59 | |
40 | +/svn/!svn/ver/9818/xmlrpcplugin/trunk/tracrpc/tests/wiki.py | |
41 | +END | |
42 | +xml_rpc.py | |
43 | +K 25 | |
44 | +svn:wc:ra_dav:version-url | |
45 | +V 62 | |
46 | +/svn/!svn/ver/9262/xmlrpcplugin/trunk/tracrpc/tests/xml_rpc.py | |
47 | +END |
@@ -0,0 +1,266 @@ | ||
1 | +10 | |
2 | + | |
3 | +dir | |
4 | +10247 | |
5 | +http://trac-hacks.org/svn/xmlrpcplugin/trunk/tracrpc/tests | |
6 | +http://trac-hacks.org/svn | |
7 | + | |
8 | + | |
9 | + | |
10 | +2011-03-02T03:58:15.256047Z | |
11 | +9913 | |
12 | +osimons | |
13 | + | |
14 | + | |
15 | + | |
16 | + | |
17 | + | |
18 | + | |
19 | + | |
20 | + | |
21 | + | |
22 | + | |
23 | + | |
24 | + | |
25 | + | |
26 | + | |
27 | +7322e99d-02ea-0310-aa39-e9a107903beb | |
28 | + | |
29 | +api.py | |
30 | +file | |
31 | + | |
32 | + | |
33 | + | |
34 | + | |
35 | +2010-05-02T23:49:30.648005Z | |
36 | +9892e99b01b70e6ef59b06391a6c42af | |
37 | +2010-05-02T23:49:30.648005Z | |
38 | +7916 | |
39 | +osimons | |
40 | + | |
41 | + | |
42 | + | |
43 | + | |
44 | + | |
45 | + | |
46 | + | |
47 | + | |
48 | + | |
49 | + | |
50 | + | |
51 | + | |
52 | + | |
53 | + | |
54 | + | |
55 | + | |
56 | + | |
57 | + | |
58 | + | |
59 | + | |
60 | + | |
61 | +5519 | |
62 | + | |
63 | +web_ui.py | |
64 | +file | |
65 | + | |
66 | + | |
67 | + | |
68 | + | |
69 | +2010-05-21T10:35:35.541189Z | |
70 | +d653c2922853a1cfe7c8e30df003f0eb | |
71 | +2010-05-21T10:35:35.541189Z | |
72 | +7971 | |
73 | +osimons | |
74 | + | |
75 | + | |
76 | + | |
77 | + | |
78 | + | |
79 | + | |
80 | + | |
81 | + | |
82 | + | |
83 | + | |
84 | + | |
85 | + | |
86 | + | |
87 | + | |
88 | + | |
89 | + | |
90 | + | |
91 | + | |
92 | + | |
93 | + | |
94 | + | |
95 | +4778 | |
96 | + | |
97 | +json_rpc.py | |
98 | +file | |
99 | + | |
100 | + | |
101 | + | |
102 | + | |
103 | +2010-05-18T22:08:17.428696Z | |
104 | +e4c6ba6a4c1fcf9718172edc88c1987d | |
105 | +2010-05-18T22:08:17.428696Z | |
106 | +7966 | |
107 | +osimons | |
108 | + | |
109 | + | |
110 | + | |
111 | + | |
112 | + | |
113 | + | |
114 | + | |
115 | + | |
116 | + | |
117 | + | |
118 | + | |
119 | + | |
120 | + | |
121 | + | |
122 | + | |
123 | + | |
124 | + | |
125 | + | |
126 | + | |
127 | + | |
128 | + | |
129 | +9894 | |
130 | + | |
131 | +__init__.py | |
132 | +file | |
133 | + | |
134 | + | |
135 | + | |
136 | + | |
137 | +2010-05-18T22:08:17.428696Z | |
138 | +fbd961a14031adfaf8e56c5c899973fe | |
139 | +2010-05-18T22:08:17.428696Z | |
140 | +7966 | |
141 | +osimons | |
142 | + | |
143 | + | |
144 | + | |
145 | + | |
146 | + | |
147 | + | |
148 | + | |
149 | + | |
150 | + | |
151 | + | |
152 | + | |
153 | + | |
154 | + | |
155 | + | |
156 | + | |
157 | + | |
158 | + | |
159 | + | |
160 | + | |
161 | + | |
162 | + | |
163 | +4878 | |
164 | + | |
165 | +ticket.py | |
166 | +file | |
167 | + | |
168 | + | |
169 | + | |
170 | + | |
171 | +2011-03-02T03:58:15.256047Z | |
172 | +c5a83de2c9a5e692783894f4119b5b22 | |
173 | +2011-03-02T03:58:15.256047Z | |
174 | +9913 | |
175 | +osimons | |
176 | + | |
177 | + | |
178 | + | |
179 | + | |
180 | + | |
181 | + | |
182 | + | |
183 | + | |
184 | + | |
185 | + | |
186 | + | |
187 | + | |
188 | + | |
189 | + | |
190 | + | |
191 | + | |
192 | + | |
193 | + | |
194 | + | |
195 | + | |
196 | + | |
197 | +15205 | |
198 | + | |
199 | +wiki.py | |
200 | +file | |
201 | + | |
202 | + | |
203 | + | |
204 | + | |
205 | +2011-02-04T00:18:16.742902Z | |
206 | +14f37d34cf35d2aac4a6e77137829dfd | |
207 | +2011-02-04T00:18:16.742902Z | |
208 | +9818 | |
209 | +osimons | |
210 | + | |
211 | + | |
212 | + | |
213 | + | |
214 | + | |
215 | + | |
216 | + | |
217 | + | |
218 | + | |
219 | + | |
220 | + | |
221 | + | |
222 | + | |
223 | + | |
224 | + | |
225 | + | |
226 | + | |
227 | + | |
228 | + | |
229 | + | |
230 | + | |
231 | +3920 | |
232 | + | |
233 | +xml_rpc.py | |
234 | +file | |
235 | + | |
236 | + | |
237 | + | |
238 | + | |
239 | +2010-10-07T18:09:30.812825Z | |
240 | +9a097de8b3fa9a155ae8f89369568020 | |
241 | +2010-10-07T18:09:30.812825Z | |
242 | +9262 | |
243 | +osimons | |
244 | + | |
245 | + | |
246 | + | |
247 | + | |
248 | + | |
249 | + | |
250 | + | |
251 | + | |
252 | + | |
253 | + | |
254 | + | |
255 | + | |
256 | + | |
257 | + | |
258 | + | |
259 | + | |
260 | + | |
261 | + | |
262 | + | |
263 | + | |
264 | + | |
265 | +4817 | |
266 | + |
@@ -0,0 +1,121 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import unittest | |
9 | +import os | |
10 | +import time | |
11 | +import urllib2 | |
12 | + | |
13 | +try: | |
14 | + from trac.tests.functional.svntestenv import SvnFunctionalTestEnvironment | |
15 | + | |
16 | + if not hasattr(SvnFunctionalTestEnvironment, 'init'): | |
17 | + raise Exception("\nTrac version is out of date. " \ | |
18 | + "Tests require minimum Trac 0.11.5dev r8303 to run.") | |
19 | + | |
20 | + class RpcTestEnvironment(SvnFunctionalTestEnvironment): | |
21 | + | |
22 | + def __del__(self): | |
23 | + print "\nStopping web server...\n" | |
24 | + self.stop() | |
25 | + if hasattr(SvnFunctionalTestEnvironment, '__del__'): | |
26 | + SvnFunctionalTestEnvironment.__del__(self) | |
27 | + | |
28 | + def init(self): | |
29 | + self.trac_src = os.path.realpath(os.path.join( | |
30 | + __import__('trac', []).__file__, '..' , '..')) | |
31 | + print "\nFound Trac source: %s" % self.trac_src | |
32 | + SvnFunctionalTestEnvironment.init(self) | |
33 | + self.url = "%s:%s" % (self.url, self.port) | |
34 | + | |
35 | + def post_create(self, env): | |
36 | + print "Enabling RPC plugin and permissions..." | |
37 | + env.config.set('components', 'tracrpc.*', 'enabled') | |
38 | + env.config.save() | |
39 | + self.getLogger = lambda : env.log | |
40 | + self._tracadmin('permission', 'add', 'anonymous', 'XML_RPC') | |
41 | + print "Created test environment: %s" % self.dirname | |
42 | + parts = urllib2.urlparse.urlsplit(self.url) | |
43 | + # Regular URIs | |
44 | + self.url_anon = '%s://%s/rpc' % (parts[0], parts[1]) | |
45 | + self.url_auth = '%s://%s/login/rpc' % (parts[0], parts[1]) | |
46 | + # URIs with user:pass as part of URL | |
47 | + self.url_user = '%s://user:user@%s/login/xmlrpc' % \ | |
48 | + (parts[0], parts[1]) | |
49 | + self.url_admin = '%s://admin:admin@%s/login/xmlrpc' % \ | |
50 | + (parts[0], parts[1]) | |
51 | + SvnFunctionalTestEnvironment.post_create(self, env) | |
52 | + print "Starting web server: %s" % self.url | |
53 | + self.restart() | |
54 | + | |
55 | + def _tracadmin(self, *args, **kwargs): | |
56 | + do_wait = kwargs.pop('wait', False) | |
57 | + SvnFunctionalTestEnvironment._tracadmin(self, *args, **kwargs) | |
58 | + if do_wait: # Delay to ensure command executes and caches resets | |
59 | + time.sleep(5) | |
60 | + | |
61 | + rpc_testenv = RpcTestEnvironment(os.path.realpath(os.path.join( | |
62 | + os.path.realpath(__file__), '..', '..', '..', 'rpctestenv')), | |
63 | + '8765', 'http://127.0.0.1') | |
64 | + | |
65 | + def test_suite(): | |
66 | + suite = unittest.TestSuite() | |
67 | + import tracrpc.tests.api | |
68 | + suite.addTest(tracrpc.tests.api.test_suite()) | |
69 | + import tracrpc.tests.xml_rpc | |
70 | + suite.addTest(tracrpc.tests.xml_rpc.test_suite()) | |
71 | + import tracrpc.tests.json_rpc | |
72 | + suite.addTest(tracrpc.tests.json_rpc.test_suite()) | |
73 | + import tracrpc.tests.ticket | |
74 | + suite.addTest(tracrpc.tests.ticket.test_suite()) | |
75 | + import tracrpc.tests.wiki | |
76 | + suite.addTest(tracrpc.tests.wiki.test_suite()) | |
77 | + import tracrpc.tests.web_ui | |
78 | + suite.addTest(tracrpc.tests.web_ui.test_suite()) | |
79 | + return suite | |
80 | + | |
81 | +except Exception, e: | |
82 | + print e | |
83 | + print "Trac test infrastructure not available." | |
84 | + print "Install Trac as 'python setup.py develop' (run Trac from source).\n" | |
85 | + test_suite = unittest.TestSuite() # return empty suite | |
86 | + | |
87 | + TracRpcTestCase = unittest.TestCase | |
88 | +else : | |
89 | + __unittest = 1 # Do not show this module in tracebacks | |
90 | + class TracRpcTestCase(unittest.TestCase): | |
91 | + def setUp(self): | |
92 | + log = rpc_testenv.get_trac_environment().log | |
93 | + log.info('=' * 70) | |
94 | + log.info('(TEST) Starting %s.%s', | |
95 | + self.__class__.__name__, | |
96 | + self._testMethodName) | |
97 | + log.info('=' * 70) | |
98 | + | |
99 | + def failUnlessRaises(self, excClass, callableObj, *args, **kwargs): | |
100 | + """Enhanced assertions to detect exceptions.""" | |
101 | + try: | |
102 | + callableObj(*args, **kwargs) | |
103 | + except excClass, e: | |
104 | + return e | |
105 | + except self.failureException : | |
106 | + raise | |
107 | + except Exception, e : | |
108 | + if hasattr(excClass, '__name__'): excName = excClass.__name__ | |
109 | + else: excName = str(excClass) | |
110 | + | |
111 | + if hasattr(e, '__name__'): excMsg = e.__name__ | |
112 | + else: excMsg = str(e) | |
113 | + | |
114 | + raise self.failureException("\n\nExpected %s\n\nGot %s : %s" % ( | |
115 | + excName, e.__class__.__name__, excMsg)) | |
116 | + else: | |
117 | + if hasattr(excClass,'__name__'): excName = excClass.__name__ | |
118 | + else: excName = str(excClass) | |
119 | + raise self.failureException, "Expected %s\n\nNothing raised" % excName | |
120 | + | |
121 | + assertRaises = failUnlessRaises |
@@ -0,0 +1,130 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import os | |
9 | +import unittest | |
10 | +import urllib2 | |
11 | + | |
12 | +from tracrpc.tests import rpc_testenv, TracRpcTestCase | |
13 | + | |
14 | +from tracrpc.api import IRPCProtocol | |
15 | + | |
16 | +from trac.core import * | |
17 | +from trac.test import Mock | |
18 | + | |
19 | +class ProtocolProviderTestCase(TracRpcTestCase): | |
20 | + | |
21 | + def setUp(self): | |
22 | + TracRpcTestCase.setUp(self) | |
23 | + | |
24 | + def tearDown(self): | |
25 | + TracRpcTestCase.tearDown(self) | |
26 | + | |
27 | + def test_invalid_content_type(self): | |
28 | + req = urllib2.Request(rpc_testenv.url_anon, | |
29 | + headers={'Content-Type': 'text/plain'}, | |
30 | + data='Fail! No RPC for text/plain') | |
31 | + try: | |
32 | + resp = urllib2.urlopen(req) | |
33 | + self.fail("Expected urllib2.HTTPError") | |
34 | + except urllib2.HTTPError, e: | |
35 | + self.assertEquals(e.code, 415) | |
36 | + self.assertEquals(e.msg, "Unsupported Media Type") | |
37 | + self.assertEquals(e.fp.read(), | |
38 | + "No protocol matching Content-Type 'text/plain' at path '/rpc'.") | |
39 | + | |
40 | + def test_valid_provider(self): | |
41 | + # Confirm the request won't work before adding plugin | |
42 | + req = urllib2.Request(rpc_testenv.url_anon, | |
43 | + headers={'Content-Type': 'application/x-tracrpc-test'}, | |
44 | + data="Fail! No RPC for application/x-tracrpc-test") | |
45 | + try: | |
46 | + resp = urllib2.urlopen(req) | |
47 | + self.fail("Expected urllib2.HTTPError") | |
48 | + except urllib2.HTTPError, e: | |
49 | + self.assertEquals(e.code, 415) | |
50 | + # Make a new plugin | |
51 | + provider = os.path.join(rpc_testenv.tracdir, 'plugins', 'DummyProvider.py') | |
52 | + open(provider, 'w').write( | |
53 | + "from trac.core import *\n" | |
54 | + "from tracrpc.api import *\n" | |
55 | + "class DummyProvider(Component):\n" | |
56 | + " implements(IRPCProtocol)\n" | |
57 | + " def rpc_info(self):\n" | |
58 | + " return ('TEST-RPC', 'No Docs!')\n" | |
59 | + " def rpc_match(self):\n" | |
60 | + " yield ('rpc', 'application/x-tracrpc-test')\n" | |
61 | + " def parse_rpc_request(self, req, content_type):\n" | |
62 | + " return {'method' : 'system.getAPIVersion'}\n" | |
63 | + " def send_rpc_error(self, req, e):\n" | |
64 | + " rpcreq = req.rpc\n" | |
65 | + " req.send_error(None, template='', content_type=rpcreq['mimetype'],\n" | |
66 | + " status=500, env=None, data='Test failure ')\n" | |
67 | + " def send_rpc_result(self, req, result):\n" | |
68 | + " rpcreq = req.rpc\n" | |
69 | + " # raise KeyError('Here')\n" | |
70 | + " response = 'Got a result!'\n" | |
71 | + " req.send(response, rpcreq['mimetype'], 200)\n") | |
72 | + rpc_testenv.restart() | |
73 | + try: | |
74 | + req = urllib2.Request(rpc_testenv.url_anon, | |
75 | + headers={'Content-Type': 'application/x-tracrpc-test'}) | |
76 | + resp = urllib2.urlopen(req) | |
77 | + self.assertEquals(200, resp.code) | |
78 | + self.assertEquals("Got a result!", resp.read()) | |
79 | + self.assertEquals(resp.headers['Content-Type'], | |
80 | + 'application/x-tracrpc-test;charset=utf-8') | |
81 | + finally: | |
82 | + # Clean up so that provider don't affect further tests | |
83 | + os.unlink(provider) | |
84 | + rpc_testenv.restart() | |
85 | + | |
86 | + def test_general_provider_error(self): | |
87 | + # Make a new plugin and restart server | |
88 | + provider = os.path.join(rpc_testenv.tracdir, 'plugins', 'DummyProvider.py') | |
89 | + open(provider, 'w').write( | |
90 | + "from trac.core import *\n" | |
91 | + "from tracrpc.api import *\n" | |
92 | + "class DummyProvider(Component):\n" | |
93 | + " implements(IRPCProtocol)\n" | |
94 | + " def rpc_info(self):\n" | |
95 | + " return ('TEST-RPC', 'No Docs!')\n" | |
96 | + " def rpc_match(self):\n" | |
97 | + " yield ('rpc', 'application/x-tracrpc-test')\n" | |
98 | + " def parse_rpc_request(self, req, content_type):\n" | |
99 | + " return {'method' : 'system.getAPIVersion'}\n" | |
100 | + " def send_rpc_error(self, req, e):\n" | |
101 | + " if isinstance(e, RPCError) :\n" | |
102 | + " req.send_error(None, template='', \n" | |
103 | + " content_type='text/plain',\n" | |
104 | + " status=500, env=None, data=e.message)\n" | |
105 | + " else :\n" | |
106 | + " req.send_error(None, template='', \n" | |
107 | + " content_type='text/plain',\n" | |
108 | + " status=500, env=None, data='Test failure')\n" | |
109 | + " def send_rpc_result(self, req, result):\n" | |
110 | + " raise RPCError('No good.')") | |
111 | + rpc_testenv.restart() | |
112 | + # Make the request | |
113 | + try: | |
114 | + req = urllib2.Request(rpc_testenv.url_anon, | |
115 | + headers={'Content-Type': 'application/x-tracrpc-test'}) | |
116 | + resp = urllib2.urlopen(req) | |
117 | + except urllib2.HTTPError, e: | |
118 | + self.assertEquals(500, e.code) | |
119 | + self.assertEquals("No good.", e.fp.read()) | |
120 | + self.assertTrue(e.hdrs['Content-Type'].startswith('text/plain')) | |
121 | + finally: | |
122 | + # Clean up so that provider don't affect further tests | |
123 | + os.unlink(provider) | |
124 | + rpc_testenv.restart() | |
125 | + | |
126 | +def test_suite(): | |
127 | + return unittest.makeSuite(ProtocolProviderTestCase) | |
128 | + | |
129 | +if __name__ == '__main__': | |
130 | + unittest.main(defaultTest='test_suite') |
@@ -0,0 +1,222 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import unittest | |
9 | +import os | |
10 | +import shutil | |
11 | +import urllib2 | |
12 | + | |
13 | +from tracrpc.json_rpc import json | |
14 | +from tracrpc.util import StringIO | |
15 | + | |
16 | +from tracrpc.tests import rpc_testenv, TracRpcTestCase | |
17 | + | |
18 | +class JsonModuleAvailabilityTestCase(TracRpcTestCase): | |
19 | + | |
20 | + def setUp(self): | |
21 | + TracRpcTestCase.setUp(self) | |
22 | + | |
23 | + def tearDown(self): | |
24 | + TracRpcTestCase.tearDown(self) | |
25 | + | |
26 | + def test_json_not_available(self): | |
27 | + if not json: | |
28 | + # No json, so just make sure the protocol isn't there | |
29 | + import tracrpc.json_rpc | |
30 | + self.failIf(hasattr(tracrpc.json_rpc, 'JsonRpcProtocol'), | |
31 | + "JsonRpcProtocol really available?") | |
32 | + return | |
33 | + # Module manipulation to simulate json libs not available | |
34 | + import sys | |
35 | + old_json = sys.modules.get('json', None) | |
36 | + sys.modules['json'] = None | |
37 | + old_simplejson = sys.modules.get('simplejson', None) | |
38 | + sys.modules['simplejson'] = None | |
39 | + if 'tracrpc.json_rpc' in sys.modules: | |
40 | + del sys.modules['tracrpc.json_rpc'] | |
41 | + try: | |
42 | + import tracrpc.json_rpc | |
43 | + self.failIf(hasattr(tracrpc.json_rpc, 'JsonRpcProtocol'), | |
44 | + "JsonRpcProtocol really available?") | |
45 | + finally: | |
46 | + del sys.modules['json'] | |
47 | + del sys.modules['simplejson'] | |
48 | + if old_json: | |
49 | + sys.modules['json'] = old_json | |
50 | + if old_simplejson: | |
51 | + sys.modules['simplejson'] = old_simplejson | |
52 | + if 'tracrpc.json_rpc' in sys.modules: | |
53 | + del sys.modules['tracrpc.json_rpc'] | |
54 | + import tracrpc.json_rpc | |
55 | + self.failIf(not hasattr(tracrpc.json_rpc, 'JsonRpcProtocol'), | |
56 | + "What, no JsonRpcProtocol?") | |
57 | + | |
58 | +if not json: | |
59 | + print "SKIP: json not available. Cannot run JsonTestCase." | |
60 | + class JsonTestCase(TracRpcTestCase): | |
61 | + pass | |
62 | +else: | |
63 | + class JsonTestCase(TracRpcTestCase): | |
64 | + | |
65 | + def _anon_req(self, data): | |
66 | + req = urllib2.Request(rpc_testenv.url_anon, | |
67 | + headers={'Content-Type': 'application/json'}) | |
68 | + req.data = json.dumps(data) | |
69 | + resp = urllib2.urlopen(req) | |
70 | + return json.loads(resp.read()) | |
71 | + | |
72 | + def _auth_req(self, data, user='user'): | |
73 | + password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() | |
74 | + handler = urllib2.HTTPBasicAuthHandler(password_mgr) | |
75 | + password_mgr.add_password(realm=None, | |
76 | + uri=rpc_testenv.url_auth, | |
77 | + user=user, | |
78 | + passwd=user) | |
79 | + req = urllib2.Request(rpc_testenv.url_auth, | |
80 | + headers={'Content-Type': 'application/json'}) | |
81 | + req.data = json.dumps(data) | |
82 | + resp = urllib2.build_opener(handler).open(req) | |
83 | + return json.loads(resp.read()) | |
84 | + | |
85 | + def setUp(self): | |
86 | + TracRpcTestCase.setUp(self) | |
87 | + | |
88 | + def tearDown(self): | |
89 | + TracRpcTestCase.tearDown(self) | |
90 | + | |
91 | + def test_call(self): | |
92 | + result = self._anon_req( | |
93 | + {'method': 'system.listMethods', 'params': [], 'id': 244}) | |
94 | + self.assertTrue('system.methodHelp' in result['result']) | |
95 | + self.assertEquals(None, result['error']) | |
96 | + self.assertEquals(244, result['id']) | |
97 | + | |
98 | + def test_multicall(self): | |
99 | + data = {'method': 'system.multicall', 'params': [ | |
100 | + {'method': 'wiki.getAllPages', 'params': [], 'id': 1}, | |
101 | + {'method': 'wiki.getPage', 'params': ['WikiStart', 1], 'id': 2}, | |
102 | + {'method': 'ticket.status.getAll', 'params': [], 'id': 3}, | |
103 | + {'method': 'nonexisting', 'params': []} | |
104 | + ], 'id': 233} | |
105 | + result = self._anon_req(data) | |
106 | + self.assertEquals(None, result['error']) | |
107 | + self.assertEquals(4, len(result['result'])) | |
108 | + items = result['result'] | |
109 | + self.assertEquals(1, items[0]['id']) | |
110 | + self.assertEquals(233, items[3]['id']) | |
111 | + self.assertTrue('WikiStart' in items[0]['result']) | |
112 | + self.assertEquals(None, items[0]['error']) | |
113 | + self.assertTrue('Welcome' in items[1]['result']) | |
114 | + self.assertEquals(['accepted', 'assigned', 'closed', 'new', | |
115 | + 'reopened'], items[2]['result']) | |
116 | + self.assertEquals(None, items[3]['result']) | |
117 | + self.assertEquals('JSONRPCError', items[3]['error']['name']) | |
118 | + | |
119 | + def test_datetime(self): | |
120 | + # read and write datetime values | |
121 | + from datetime import datetime | |
122 | + from trac.util.datefmt import utc | |
123 | + dt_str = "2009-06-19T16:46:00" | |
124 | + dt_dt = datetime(2009, 06, 19, 16, 46, 00, tzinfo=utc) | |
125 | + data = {'method': 'ticket.milestone.update', | |
126 | + 'params': ['milestone1', {'due': {'__jsonclass__': | |
127 | + ['datetime', dt_str]}}]} | |
128 | + result = self._auth_req(data, user='admin') | |
129 | + self.assertEquals(None, result['error']) | |
130 | + result = self._auth_req({'method': 'ticket.milestone.get', | |
131 | + 'params': ['milestone1']}, user='admin') | |
132 | + self.assertTrue(result['result']) | |
133 | + self.assertEquals(dt_str, | |
134 | + result['result']['due']['__jsonclass__'][1]) | |
135 | + | |
136 | + def test_binary(self): | |
137 | + # read and write binaries values | |
138 | + image_url = os.path.join(rpc_testenv.trac_src, 'trac', | |
139 | + 'htdocs', 'feed.png') | |
140 | + image_in = StringIO(open(image_url, 'r').read()) | |
141 | + data = {'method': 'wiki.putAttachmentEx', | |
142 | + 'params': ['TitleIndex', "feed2.png", "test image", | |
143 | + {'__jsonclass__': ['binary', | |
144 | + image_in.getvalue().encode("base64")]}]} | |
145 | + result = self._auth_req(data, user='admin') | |
146 | + self.assertEquals('feed2.png', result['result']) | |
147 | + # Now try to get the attachment, and verify it is identical | |
148 | + result = self._auth_req({'method': 'wiki.getAttachment', | |
149 | + 'params': ['TitleIndex/feed2.png']}, user='admin') | |
150 | + self.assertTrue(result['result']) | |
151 | + image_out = StringIO( | |
152 | + result['result']['__jsonclass__'][1].decode("base64")) | |
153 | + self.assertEquals(image_in.getvalue(), image_out.getvalue()) | |
154 | + | |
155 | + def test_xmlrpc_permission(self): | |
156 | + # Test returned response if not XML_RPC permission | |
157 | + rpc_testenv._tracadmin('permission', 'remove', 'anonymous', | |
158 | + 'XML_RPC', wait=True) | |
159 | + try: | |
160 | + result = self._anon_req({'method': 'system.listMethods', | |
161 | + 'id': 'no-perm'}) | |
162 | + self.assertEquals(None, result['result']) | |
163 | + self.assertEquals('no-perm', result['id']) | |
164 | + self.assertEquals(403, result['error']['code']) | |
165 | + self.assertTrue('XML_RPC' in result['error']['message']) | |
166 | + finally: | |
167 | + # Add back the default permission for further tests | |
168 | + rpc_testenv._tracadmin('permission', 'add', 'anonymous', | |
169 | + 'XML_RPC', wait=True) | |
170 | + | |
171 | + def test_method_not_found(self): | |
172 | + result = self._anon_req({'method': 'system.doesNotExist', | |
173 | + 'id': 'no-method'}) | |
174 | + self.assertTrue(result['error']) | |
175 | + self.assertEquals(result['id'], 'no-method') | |
176 | + self.assertEquals(None, result['result']) | |
177 | + self.assertEquals(-32601, result['error']['code']) | |
178 | + self.assertTrue('not found' in result['error']['message']) | |
179 | + | |
180 | + def test_wrong_argspec(self): | |
181 | + result = self._anon_req({'method': 'system.listMethods', | |
182 | + 'params': ['hello'], 'id': 'wrong-args'}) | |
183 | + self.assertTrue(result['error']) | |
184 | + self.assertEquals(result['id'], 'wrong-args') | |
185 | + self.assertEquals(None, result['result']) | |
186 | + self.assertEquals(-32603, result['error']['code']) | |
187 | + self.assertTrue('listMethods() takes exactly 2 arguments' \ | |
188 | + in result['error']['message']) | |
189 | + | |
190 | + def test_call_permission(self): | |
191 | + # Test missing call-specific permission | |
192 | + result = self._anon_req({'method': 'ticket.component.delete', | |
193 | + 'params': ['component1'], 'id': 2332}) | |
194 | + self.assertEquals(None, result['result']) | |
195 | + self.assertEquals(2332, result['id']) | |
196 | + self.assertEquals(403, result['error']['code']) | |
197 | + self.assertEquals(result['error']['message'], | |
198 | + 'TICKET_ADMIN privileges are required to perform this operation') | |
199 | + | |
200 | + def test_resource_not_found(self): | |
201 | + # A Ticket resource | |
202 | + result = self._anon_req({'method': 'ticket.get', | |
203 | + 'params': [2147483647], 'id': 3443}) | |
204 | + self.assertEquals(result['id'], 3443) | |
205 | + self.assertEquals(result['error']['code'], 404) | |
206 | + self.assertEquals(result['error']['message'], | |
207 | + 'Ticket 2147483647 does not exist.') | |
208 | + # A Wiki resource | |
209 | + result = self._anon_req({'method': 'wiki.getPage', | |
210 | + 'params': ["Test", 10], 'id': 3443}) | |
211 | + self.assertEquals(result['error']['code'], 404) | |
212 | + self.assertEquals(result['error']['message'], | |
213 | + 'Wiki page "Test" does not exist at version 10') | |
214 | + | |
215 | +def test_suite(): | |
216 | + test_suite = unittest.TestSuite() | |
217 | + test_suite.addTest(unittest.makeSuite(JsonModuleAvailabilityTestCase)) | |
218 | + test_suite.addTest(unittest.makeSuite(JsonTestCase)) | |
219 | + return test_suite | |
220 | + | |
221 | +if __name__ == '__main__': | |
222 | + unittest.main(defaultTest='test_suite') |
@@ -0,0 +1,335 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import unittest | |
9 | + | |
10 | +import xmlrpclib | |
11 | +import os | |
12 | +import shutil | |
13 | +import datetime | |
14 | +import time | |
15 | + | |
16 | +from tracrpc.tests import rpc_testenv, TracRpcTestCase | |
17 | + | |
18 | +class RpcTicketTestCase(TracRpcTestCase): | |
19 | + | |
20 | + def setUp(self): | |
21 | + TracRpcTestCase.setUp(self) | |
22 | + self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) | |
23 | + self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) | |
24 | + self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) | |
25 | + | |
26 | + def tearDown(self): | |
27 | + TracRpcTestCase.tearDown(self) | |
28 | + | |
29 | + def test_getActions(self): | |
30 | + tid = self.admin.ticket.create("ticket_getActions", "kjsald", {}) | |
31 | + actions = self.admin.ticket.getActions(tid) | |
32 | + default = [['leave', 'leave', '.', []], ['resolve', 'resolve', | |
33 | + "The resolution will be set. Next status will be 'closed'.", | |
34 | + [['action_resolve_resolve_resolution', 'fixed', | |
35 | + ['fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme']]]], | |
36 | + ['reassign', 'reassign', | |
37 | + "The owner will change from (none). Next status will be 'assigned'.", | |
38 | + [['action_reassign_reassign_owner', 'admin', []]]], | |
39 | + ['accept', 'accept', | |
40 | + "The owner will change from (none) to admin. Next status will be 'accepted'.", []]] | |
41 | + # Some action text was changed in trac:changeset:9041 - adjust default for test | |
42 | + if 'will be changed' in actions[2][2]: | |
43 | + default[2][2] = default[2][2].replace('will change', 'will be changed') | |
44 | + default[3][2] = default[3][2].replace('will change', 'will be changed') | |
45 | + self.assertEquals(actions, default) | |
46 | + self.admin.ticket.delete(tid) | |
47 | + | |
48 | + def test_getAvailableActions_DeleteTicket(self): | |
49 | + # http://trac-hacks.org/ticket/5387 | |
50 | + tid = self.admin.ticket.create('abc', 'def', {}) | |
51 | + self.assertEquals(False, | |
52 | + 'delete' in self.admin.ticket.getAvailableActions(tid)) | |
53 | + env = rpc_testenv.get_trac_environment() | |
54 | + delete_plugin = os.path.join(rpc_testenv.tracdir, | |
55 | + 'plugins', 'DeleteTicket.py') | |
56 | + shutil.copy(os.path.join( | |
57 | + rpc_testenv.trac_src, 'sample-plugins', 'workflow', 'DeleteTicket.py'), | |
58 | + delete_plugin) | |
59 | + env.config.set('ticket', 'workflow', | |
60 | + 'ConfigurableTicketWorkflow,DeleteTicketActionController') | |
61 | + env.config.save() | |
62 | + self.assertEquals(True, | |
63 | + 'delete' in self.admin.ticket.getAvailableActions(tid)) | |
64 | + self.assertEquals(False, | |
65 | + 'delete' in self.user.ticket.getAvailableActions(tid)) | |
66 | + env.config.set('ticket', 'workflow', | |
67 | + 'ConfigurableTicketWorkflow') | |
68 | + env.config.save() | |
69 | + rpc_testenv.restart() | |
70 | + self.assertEquals(False, | |
71 | + 'delete' in self.admin.ticket.getAvailableActions(tid)) | |
72 | + # Clean up | |
73 | + os.unlink(delete_plugin) | |
74 | + rpc_testenv.restart() | |
75 | + | |
76 | + def test_FineGrainedSecurity(self): | |
77 | + self.assertEquals(1, self.admin.ticket.create('abc', '123', {})) | |
78 | + self.assertEquals(2, self.admin.ticket.create('def', '456', {})) | |
79 | + # First some non-restricted tests for comparison: | |
80 | + self.assertRaises(xmlrpclib.Fault, self.anon.ticket.create, 'abc', 'def') | |
81 | + self.assertEquals([1,2], self.user.ticket.query()) | |
82 | + self.assertTrue(self.user.ticket.get(2)) | |
83 | + self.assertTrue(self.user.ticket.update(1, "ok")) | |
84 | + self.assertTrue(self.user.ticket.update(2, "ok")) | |
85 | + # Enable security policy and test | |
86 | + from trac.core import Component, implements | |
87 | + from trac.perm import IPermissionPolicy | |
88 | + policy = os.path.join(rpc_testenv.tracdir, 'plugins', 'TicketPolicy.py') | |
89 | + open(policy, 'w').write( | |
90 | + "from trac.core import *\n" | |
91 | + "from trac.perm import IPermissionPolicy\n" | |
92 | + "class TicketPolicy(Component):\n" | |
93 | + " implements(IPermissionPolicy)\n" | |
94 | + " def check_permission(self, action, username, resource, perm):\n" | |
95 | + " if username == 'user' and resource and resource.id == 2:\n" | |
96 | + " return False\n" | |
97 | + " if username == 'anonymous' and action == 'TICKET_CREATE':\n" | |
98 | + " return True\n") | |
99 | + env = rpc_testenv.get_trac_environment() | |
100 | + _old_conf = env.config.get('trac', 'permission_policies') | |
101 | + env.config.set('trac', 'permission_policies', 'TicketPolicy,'+_old_conf) | |
102 | + env.config.save() | |
103 | + rpc_testenv.restart() | |
104 | + self.assertEquals([1], self.user.ticket.query()) | |
105 | + self.assertTrue(self.user.ticket.get(1)) | |
106 | + self.assertRaises(xmlrpclib.Fault, self.user.ticket.get, 2) | |
107 | + self.assertTrue(self.user.ticket.update(1, "ok")) | |
108 | + self.assertRaises(xmlrpclib.Fault, self.user.ticket.update, 2, "not ok") | |
109 | + self.assertEquals(3, self.anon.ticket.create('efg', '789', {})) | |
110 | + # Clean, reset and simple verification | |
111 | + env.config.set('trac', 'permission_policies', _old_conf) | |
112 | + env.config.save() | |
113 | + os.unlink(policy) | |
114 | + rpc_testenv.restart() | |
115 | + self.assertEquals([1,2,3], self.user.ticket.query()) | |
116 | + self.assertEquals(0, self.admin.ticket.delete(1)) | |
117 | + self.assertEquals(0, self.admin.ticket.delete(2)) | |
118 | + self.assertEquals(0, self.admin.ticket.delete(3)) | |
119 | + | |
120 | + def test_getRecentChanges(self): | |
121 | + tid1 = self.admin.ticket.create("ticket_getRecentChanges", "one", {}) | |
122 | + time.sleep(1) | |
123 | + tid2 = self.admin.ticket.create("ticket_getRecentChanges", "two", {}) | |
124 | + _id, created, modified, attributes = self.admin.ticket.get(tid2) | |
125 | + changes = self.admin.ticket.getRecentChanges(created) | |
126 | + try: | |
127 | + self.assertEquals(changes, [tid2]) | |
128 | + finally: | |
129 | + self.admin.ticket.delete(tid1) | |
130 | + self.admin.ticket.delete(tid2) | |
131 | + | |
132 | + def test_query_special_character_escape(self): | |
133 | + # Note: This test only passes when using Trac 0.12+ | |
134 | + # See http://trac-hacks.org/ticket/7737 | |
135 | + if __import__('trac').__version__ < '0.12': | |
136 | + self.fail("Known issue: Trac 0.11 does not handle escaped input properly.") | |
137 | + summary = ["here&now", "maybe|later", "back\slash"] | |
138 | + search = ["here\&now", "maybe\|later", "back\\slash"] | |
139 | + tids = [] | |
140 | + for s in summary: | |
141 | + tids.append(self.admin.ticket.create(s, | |
142 | + "test_special_character_escape", {})) | |
143 | + try: | |
144 | + for i in range(0, 3): | |
145 | + self.assertEquals([tids[i]], | |
146 | + self.admin.ticket.query("summary=%s" % search[i])) | |
147 | + self.assertEquals(tids.sort(), | |
148 | + self.admin.ticket.query("summary=%s" % "|".join(search)).sort()) | |
149 | + finally: | |
150 | + for tid in tids: | |
151 | + self.admin.ticket.delete(tid) | |
152 | + | |
153 | + def test_update_author(self): | |
154 | + tid = self.admin.ticket.create("ticket_update_author", "one", {}) | |
155 | + self.admin.ticket.update(tid, 'comment1', {}) | |
156 | + time.sleep(1) | |
157 | + self.admin.ticket.update(tid, 'comment2', {}, False, 'foo') | |
158 | + time.sleep(1) | |
159 | + self.user.ticket.update(tid, 'comment3', {}, False, 'should_be_rejected') | |
160 | + changes = self.admin.ticket.changeLog(tid) | |
161 | + self.assertEquals(3, len(changes)) | |
162 | + for when, who, what, cnum, comment, _tid in changes: | |
163 | + self.assertTrue(comment in ('comment1', 'comment2', 'comment3')) | |
164 | + if comment == 'comment1': | |
165 | + self.assertEquals('admin', who) | |
166 | + if comment == 'comment2': | |
167 | + self.assertEquals('foo', who) | |
168 | + if comment == 'comment3': | |
169 | + self.assertEquals('user', who) | |
170 | + self.admin.ticket.delete(tid) | |
171 | + | |
172 | + def test_create_at_time(self): | |
173 | + from trac.util.datefmt import to_datetime, utc | |
174 | + now = to_datetime(None, utc) | |
175 | + minus1 = now - datetime.timedelta(days=1) | |
176 | + # create the tickets (user ticket will not be permitted to change time) | |
177 | + one = self.admin.ticket.create("create_at_time1", "ok", {}, False, | |
178 | + xmlrpclib.DateTime(minus1)) | |
179 | + two = self.user.ticket.create("create_at_time3", "ok", {}, False, | |
180 | + xmlrpclib.DateTime(minus1)) | |
181 | + # get the tickets | |
182 | + t1 = self.admin.ticket.get(one) | |
183 | + t2 = self.admin.ticket.get(two) | |
184 | + # check timestamps | |
185 | + self.assertTrue(t1[1] < t2[1]) | |
186 | + self.admin.ticket.delete(one) | |
187 | + self.admin.ticket.delete(two) | |
188 | + | |
189 | + def test_update_at_time(self): | |
190 | + from trac.util.datefmt import to_datetime, utc | |
191 | + now = to_datetime(None, utc) | |
192 | + minus1 = now - datetime.timedelta(hours=1) | |
193 | + minus2 = now - datetime.timedelta(hours=2) | |
194 | + tid = self.admin.ticket.create("ticket_update_at_time", "ok", {}) | |
195 | + self.admin.ticket.update(tid, 'one', {}, False, '', xmlrpclib.DateTime(minus2)) | |
196 | + self.admin.ticket.update(tid, 'two', {}, False, '', xmlrpclib.DateTime(minus1)) | |
197 | + self.user.ticket.update(tid, 'three', {}, False, '', xmlrpclib.DateTime(minus1)) | |
198 | + time.sleep(1) | |
199 | + self.user.ticket.update(tid, 'four', {}) | |
200 | + changes = self.admin.ticket.changeLog(tid) | |
201 | + self.assertEquals(4, len(changes)) | |
202 | + # quick test to make sure each is older than previous | |
203 | + self.assertTrue(changes[0][0] < changes[1][0] < changes[2][0]) | |
204 | + # margin of 2 seconds for tests | |
205 | + justnow = xmlrpclib.DateTime(now - datetime.timedelta(seconds=1)) | |
206 | + self.assertTrue(justnow <= changes[2][0]) | |
207 | + self.assertTrue(justnow <= changes[3][0]) | |
208 | + self.admin.ticket.delete(tid) | |
209 | + | |
210 | + def test_update_non_existing(self): | |
211 | + try: | |
212 | + self.admin.ticket.update(3344, "a comment", {}) | |
213 | + self.fail("Allowed to update non-existing ticket???") | |
214 | + self.admin.ticket.delete(3234) | |
215 | + except Exception, e: | |
216 | + self.assertTrue("Ticket 3344 does not exist." in str(e)) | |
217 | + | |
218 | + def test_update_basic(self): | |
219 | + import time | |
220 | + # Basic update check, no 'action' or 'time_changed' | |
221 | + tid = self.admin.ticket.create('test_update_basic1', 'ieidnsj', { | |
222 | + 'owner': 'osimons'}) | |
223 | + # old-style (deprecated) | |
224 | + self.admin.ticket.update(tid, "comment1", {'component': 'component2'}) | |
225 | + self.assertEquals(2, len(self.admin.ticket.changeLog(tid))) | |
226 | + # new-style with 'action' | |
227 | + time.sleep(1) # avoid "columns ticket, time, field are not unique" | |
228 | + self.admin.ticket.update(tid, "comment2", {'component': 'component1', | |
229 | + 'action': 'leave'}) | |
230 | + self.assertEquals(4, len(self.admin.ticket.changeLog(tid))) | |
231 | + self.admin.ticket.delete(tid) | |
232 | + | |
233 | + def test_update_time_changed(self): | |
234 | + # Update with collision check | |
235 | + import datetime | |
236 | + from tracrpc.xml_rpc import from_xmlrpc_datetime, to_xmlrpc_datetime | |
237 | + tid = self.admin.ticket.create('test_update_time_changed', '...', {}) | |
238 | + tid, created, modified, attrs = self.admin.ticket.get(tid) | |
239 | + then = from_xmlrpc_datetime(modified) - datetime.timedelta(minutes=1) | |
240 | + # Unrestricted old-style update (to be removed soon) | |
241 | + try: | |
242 | + self.admin.ticket.update(tid, "comment1", | |
243 | + {'_ts': to_xmlrpc_datetime(then)}) | |
244 | + except Exception, e: | |
245 | + self.assertTrue("Ticket has been updated since last get" in str(e)) | |
246 | + # Update with 'action' to test new-style update. | |
247 | + try: | |
248 | + self.admin.ticket.update(tid, "comment1", | |
249 | + {'_ts': to_xmlrpc_datetime(then), | |
250 | + 'action': 'leave'}) | |
251 | + except Exception, e: | |
252 | + self.assertTrue("modified by someone else" in str(e)) | |
253 | + self.admin.ticket.delete(tid) | |
254 | + | |
255 | + def test_update_time_same(self): | |
256 | + # Update with collision check | |
257 | + import datetime | |
258 | + from tracrpc.xml_rpc import from_xmlrpc_datetime, to_xmlrpc_datetime | |
259 | + | |
260 | + # Unrestricted old-style update (to be removed soon) | |
261 | + tid = self.admin.ticket.create('test_update_time_same', '...', {}) | |
262 | + tid, created, modified, attrs = self.admin.ticket.get(tid) | |
263 | + ts = attrs['_ts'] | |
264 | + self.admin.ticket.update(tid, "comment1", | |
265 | + {'_ts': ts}) | |
266 | + self.admin.ticket.delete(tid) | |
267 | + | |
268 | + # Update with 'action' to test new-style update. | |
269 | + tid = self.admin.ticket.create('test_update_time_same', '...', {}) | |
270 | + tid, created, modified, attrs = self.admin.ticket.get(tid) | |
271 | + ts = attrs['_ts'] | |
272 | + self.admin.ticket.update(tid, "comment1", | |
273 | + {'_ts': ts, 'action': 'leave'}) | |
274 | + self.admin.ticket.delete(tid) | |
275 | + | |
276 | + def test_update_action(self): | |
277 | + # Updating with 'action' in attributes | |
278 | + tid = self.admin.ticket.create('test_update_action', 'ss') | |
279 | + current = self.admin.ticket.get(tid) | |
280 | + self.assertEqual('', current[3].get('owner', '')) | |
281 | + updated = self.admin.ticket.update(tid, "comment1", | |
282 | + {'action': 'reassign', | |
283 | + 'action_reassign_reassign_owner': 'user'}) | |
284 | + self.assertEqual('user', updated[3].get('owner')) | |
285 | + self.admin.ticket.delete(tid) | |
286 | + | |
287 | + def test_update_action_non_existing(self): | |
288 | + # Updating with non-existing 'action' in attributes | |
289 | + tid = self.admin.ticket.create('test_update_action_wrong', 'ss') | |
290 | + try: | |
291 | + self.admin.ticket.update(tid, "comment1", | |
292 | + {'action': 'reassign', | |
293 | + 'action_reassign_reassign_owner': 'user'}) | |
294 | + except Exception, e: | |
295 | + self.assertTrue("invalid action" in str(e)) | |
296 | + self.admin.ticket.delete(tid) | |
297 | + | |
298 | + def test_update_field_non_existing(self): | |
299 | + tid = self.admin.ticket.create('test_update_field_non_existing', 'yw3') | |
300 | + try: | |
301 | + self.admin.ticket.update(tid, "comment1", | |
302 | + {'does_not_exist': 'eiwrjoer'}) | |
303 | + except Exception, e: | |
304 | + self.assertTrue("no such column" in str(e)) | |
305 | + self.admin.ticket.delete(tid) | |
306 | + | |
307 | + | |
308 | +class RpcTicketVersionTestCase(TracRpcTestCase): | |
309 | + | |
310 | + def setUp(self): | |
311 | + TracRpcTestCase.setUp(self) | |
312 | + self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) | |
313 | + self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) | |
314 | + self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) | |
315 | + | |
316 | + def tearDown(self): | |
317 | + TracRpcTestCase.tearDown(self) | |
318 | + | |
319 | + def test_create(self): | |
320 | + dt = xmlrpclib.DateTime(datetime.datetime.utcnow()) | |
321 | + desc = "test version" | |
322 | + v = self.admin.ticket.version.create('9.99', | |
323 | + {'time': dt, 'description': desc}) | |
324 | + self.failUnless('9.99' in self.admin.ticket.version.getAll()) | |
325 | + self.assertEquals({'time': dt, 'description': desc, 'name': '9.99'}, | |
326 | + self.admin.ticket.version.get('9.99')) | |
327 | + | |
328 | +def test_suite(): | |
329 | + test_suite = unittest.TestSuite() | |
330 | + test_suite.addTest(unittest.makeSuite(RpcTicketTestCase)) | |
331 | + test_suite.addTest(unittest.makeSuite(RpcTicketVersionTestCase)) | |
332 | + return test_suite | |
333 | + | |
334 | +if __name__ == '__main__': | |
335 | + unittest.main(defaultTest='test_suite') |
@@ -0,0 +1,118 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import unittest | |
9 | +import urllib2 | |
10 | + | |
11 | +from tracrpc.tests import rpc_testenv, TracRpcTestCase | |
12 | + | |
13 | +class DocumentationTestCase(TracRpcTestCase): | |
14 | + | |
15 | + def setUp(self): | |
16 | + TracRpcTestCase.setUp(self) | |
17 | + password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() | |
18 | + handler = urllib2.HTTPBasicAuthHandler(password_mgr) | |
19 | + password_mgr.add_password(realm=None, | |
20 | + uri=rpc_testenv.url_auth, | |
21 | + user='user', passwd='user') | |
22 | + self.opener_user = urllib2.build_opener(handler) | |
23 | + | |
24 | + def tearDown(self): | |
25 | + TracRpcTestCase.tearDown(self) | |
26 | + | |
27 | + def test_get_with_content_type(self): | |
28 | + req = urllib2.Request(rpc_testenv.url_auth, | |
29 | + headers={'Content-Type': 'text/html'}) | |
30 | + self.assert_rpcdocs_ok(self.opener_user, req) | |
31 | + | |
32 | + def test_get_no_content_type(self): | |
33 | + req = urllib2.Request(rpc_testenv.url_auth) | |
34 | + self.assert_rpcdocs_ok(self.opener_user, req) | |
35 | + | |
36 | + def test_post_accept(self): | |
37 | + req = urllib2.Request(rpc_testenv.url_auth, | |
38 | + headers={'Content-Type' : 'text/plain', | |
39 | + 'Accept': 'application/x-trac-test,text/html'}, | |
40 | + data='Pass since client accepts HTML') | |
41 | + self.assert_rpcdocs_ok(self.opener_user, req) | |
42 | + | |
43 | + req = urllib2.Request(rpc_testenv.url_auth, | |
44 | + headers={'Content-Type' : 'text/plain'}, | |
45 | + data='Fail! No content type expected') | |
46 | + self.assert_unsupported_media_type(self.opener_user, req) | |
47 | + | |
48 | + def test_form_submit(self): | |
49 | + from urllib import urlencode | |
50 | + # Explicit content type | |
51 | + form_vars = {'result' : 'Fail! __FORM_TOKEN protection activated'} | |
52 | + req = urllib2.Request(rpc_testenv.url_auth, | |
53 | + headers={'Content-Type': 'application/x-www-form-urlencoded'}, | |
54 | + data=urlencode(form_vars)) | |
55 | + self.assert_form_protect(self.opener_user, req) | |
56 | + | |
57 | + # Implicit content type | |
58 | + req = urllib2.Request(rpc_testenv.url_auth, | |
59 | + headers={'Accept': 'application/x-trac-test,text/html'}, | |
60 | + data='Pass since client accepts HTML') | |
61 | + self.assert_form_protect(self.opener_user, req) | |
62 | + | |
63 | + def test_get_dont_accept(self): | |
64 | + req = urllib2.Request(rpc_testenv.url_auth, | |
65 | + headers={'Accept': 'application/x-trac-test'}) | |
66 | + self.assert_unsupported_media_type(self.opener_user, req) | |
67 | + | |
68 | + def test_post_dont_accept(self): | |
69 | + req = urllib2.Request(rpc_testenv.url_auth, | |
70 | + headers={'Content-Type': 'text/plain', | |
71 | + 'Accept': 'application/x-trac-test'}, | |
72 | + data='Fail! Client cannot process HTML') | |
73 | + self.assert_unsupported_media_type(self.opener_user, req) | |
74 | + | |
75 | + # Custom assertions | |
76 | + def assert_rpcdocs_ok(self, opener, req): | |
77 | + """Determine if RPC docs are ok""" | |
78 | + try : | |
79 | + resp = opener.open(req) | |
80 | + except urllib2.HTTPError, e : | |
81 | + self.fail("Request to '%s' failed (%s) %s" % (e.geturl(), | |
82 | + e.code, | |
83 | + e.fp.read())) | |
84 | + else : | |
85 | + self.assertEquals(200, resp.code) | |
86 | + body = resp.read() | |
87 | + self.assertTrue('<h3 id="XML-RPC">XML-RPC</h3>' in body) | |
88 | + self.assertTrue('<h3 id="rpc.ticket.status">' in body) | |
89 | + | |
90 | + def assert_unsupported_media_type(self, opener, req): | |
91 | + """Ensure HTTP 415 is returned back to the client""" | |
92 | + try : | |
93 | + opener.open(req) | |
94 | + except urllib2.HTTPError, e: | |
95 | + self.assertEquals(415, e.code) | |
96 | + expected = "No protocol matching Content-Type '%s' at path '%s'." % \ | |
97 | + (req.headers.get('Content-Type', 'text/plain'), | |
98 | + '/login/rpc') | |
99 | + got = e.fp.read() | |
100 | + self.assertEquals(expected, got) | |
101 | + except Exception, e: | |
102 | + self.fail('Expected HTTP error but %s raised instead' % \ | |
103 | + (e.__class__.__name__,)) | |
104 | + else : | |
105 | + self.fail('Expected HTTP error (415) but nothing raised') | |
106 | + | |
107 | + def assert_form_protect(self, opener, req): | |
108 | + e = self.assertRaises(urllib2.HTTPError, opener.open, req) | |
109 | + self.assertEquals(400, e.code) | |
110 | + msg = e.fp.read() | |
111 | + self.assertTrue("Missing or invalid form token. " | |
112 | + "Do you have cookies enabled?" in msg) | |
113 | + | |
114 | +def test_suite(): | |
115 | + return unittest.makeSuite(DocumentationTestCase) | |
116 | + | |
117 | +if __name__ == '__main__': | |
118 | + unittest.main(defaultTest='test_suite') |
@@ -0,0 +1,92 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import unittest | |
9 | + | |
10 | +import xmlrpclib | |
11 | +import os | |
12 | +import time | |
13 | + | |
14 | +from trac.util.compat import sorted | |
15 | + | |
16 | +from tracrpc.tests import rpc_testenv, TracRpcTestCase | |
17 | +from tracrpc.util import StringIO | |
18 | + | |
19 | +class RpcWikiTestCase(TracRpcTestCase): | |
20 | + | |
21 | + def setUp(self): | |
22 | + TracRpcTestCase.setUp(self) | |
23 | + self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) | |
24 | + self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) | |
25 | + self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) | |
26 | + | |
27 | + def tearDown(self): | |
28 | + TracRpcTestCase.tearDown(self) | |
29 | + | |
30 | + def test_attachments(self): | |
31 | + # Note: Quite similar to the tracrpc.tests.json.JsonTestCase.test_binary | |
32 | + image_url = os.path.join(rpc_testenv.trac_src, 'trac', | |
33 | + 'htdocs', 'feed.png') | |
34 | + image_in = StringIO(open(image_url, 'r').read()) | |
35 | + # Create attachment | |
36 | + self.admin.wiki.putAttachmentEx('TitleIndex', 'feed2.png', 'test image', | |
37 | + xmlrpclib.Binary(image_in.getvalue())) | |
38 | + self.assertEquals(image_in.getvalue(), self.admin.wiki.getAttachment( | |
39 | + 'TitleIndex/feed2.png').data) | |
40 | + # Update attachment (adding new) | |
41 | + self.admin.wiki.putAttachmentEx('TitleIndex', 'feed2.png', 'test image', | |
42 | + xmlrpclib.Binary(image_in.getvalue()), False) | |
43 | + self.assertEquals(image_in.getvalue(), self.admin.wiki.getAttachment( | |
44 | + 'TitleIndex/feed2.2.png').data) | |
45 | + # List attachments | |
46 | + self.assertEquals(['TitleIndex/feed2.2.png', 'TitleIndex/feed2.png'], | |
47 | + sorted(self.admin.wiki.listAttachments('TitleIndex'))) | |
48 | + # Delete both attachments | |
49 | + self.admin.wiki.deleteAttachment('TitleIndex/feed2.png') | |
50 | + self.admin.wiki.deleteAttachment('TitleIndex/feed2.2.png') | |
51 | + # List attachments again | |
52 | + self.assertEquals([], self.admin.wiki.listAttachments('TitleIndex')) | |
53 | + | |
54 | + def test_getRecentChanges(self): | |
55 | + self.admin.wiki.putPage('WikiOne', 'content one', {}) | |
56 | + time.sleep(1) | |
57 | + self.admin.wiki.putPage('WikiTwo', 'content two', {}) | |
58 | + attrs2 = self.admin.wiki.getPageInfo('WikiTwo') | |
59 | + changes = self.admin.wiki.getRecentChanges(attrs2['lastModified']) | |
60 | + self.assertEquals(1, len(changes)) | |
61 | + self.assertEquals('WikiTwo', changes[0]['name']) | |
62 | + self.assertEquals('admin', changes[0]['author']) | |
63 | + self.assertEquals(1, changes[0]['version']) | |
64 | + self.admin.wiki.deletePage('WikiOne') | |
65 | + self.admin.wiki.deletePage('WikiTwo') | |
66 | + | |
67 | + def test_getPageHTMLWithImage(self): | |
68 | + # Create the wiki page (absolute image reference) | |
69 | + self.admin.wiki.putPage('ImageTest', | |
70 | + '[[Image(wiki:ImageTest:feed.png, nolink)]]\n', {}) | |
71 | + # Create attachment | |
72 | + image_url = os.path.join(rpc_testenv.trac_src, 'trac', | |
73 | + 'htdocs', 'feed.png') | |
74 | + self.admin.wiki.putAttachmentEx('ImageTest', 'feed.png', 'test image', | |
75 | + xmlrpclib.Binary(open(image_url, 'r').read())) | |
76 | + # Check rendering absolute | |
77 | + markup_1 = self.admin.wiki.getPageHTML('ImageTest') | |
78 | + self.assertEquals('<html><body><p>\n<img src="http://127.0.0.1:8765' | |
79 | + '/raw-attachment/wiki/ImageTest/feed.png" alt="test image" ' | |
80 | + 'title="test image" />\n</p>\n</body></html>', markup_1) | |
81 | + # Change to relative image reference and check again | |
82 | + self.admin.wiki.putPage('ImageTest', | |
83 | + '[[Image(feed.png, nolink)]]\n', {}) | |
84 | + markup_2 = self.admin.wiki.getPageHTML('ImageTest') | |
85 | + self.assertEquals(markup_2, markup_1) | |
86 | + | |
87 | + | |
88 | +def test_suite(): | |
89 | + return unittest.makeSuite(RpcWikiTestCase) | |
90 | + | |
91 | +if __name__ == '__main__': | |
92 | + unittest.main(defaultTest='test_suite') |
@@ -0,0 +1,115 @@ | ||
1 | +# -*- coding: utf-8 -*- | |
2 | +""" | |
3 | +License: BSD | |
4 | + | |
5 | +(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) | |
6 | +""" | |
7 | + | |
8 | +import unittest | |
9 | + | |
10 | +import xmlrpclib | |
11 | + | |
12 | +from tracrpc.tests import rpc_testenv, TracRpcTestCase | |
13 | + | |
14 | +class RpcXmlTestCase(TracRpcTestCase): | |
15 | + | |
16 | + def setUp(self): | |
17 | + TracRpcTestCase.setUp(self) | |
18 | + self.anon = xmlrpclib.ServerProxy(rpc_testenv.url_anon) | |
19 | + self.user = xmlrpclib.ServerProxy(rpc_testenv.url_user) | |
20 | + self.admin = xmlrpclib.ServerProxy(rpc_testenv.url_admin) | |
21 | + | |
22 | + def tearDown(self): | |
23 | + TracRpcTestCase.tearDown(self) | |
24 | + | |
25 | + def test_xmlrpc_permission(self): | |
26 | + # Test returned response if not XML_RPC permission | |
27 | + rpc_testenv._tracadmin('permission', 'remove', 'anonymous', | |
28 | + 'XML_RPC', wait=True) | |
29 | + e = self.assertRaises(xmlrpclib.Fault, | |
30 | + self.anon.system.listMethods) | |
31 | + self.assertEquals(403, e.faultCode) | |
32 | + self.assertTrue('XML_RPC' in e.faultString) | |
33 | + rpc_testenv._tracadmin('permission', 'add', 'anonymous', | |
34 | + 'XML_RPC', wait=True) | |
35 | + | |
36 | + def test_method_not_found(self): | |
37 | + def local_test(): | |
38 | + self.admin.system.doesNotExist() | |
39 | + self.fail("What? Method exists???") | |
40 | + e = self.assertRaises(xmlrpclib.Fault, local_test) | |
41 | + self.assertEquals(-32601, e.faultCode) | |
42 | + self.assertTrue("not found" in e.faultString) | |
43 | + | |
44 | + def test_wrong_argspec(self): | |
45 | + def local_test(): | |
46 | + self.admin.system.listMethods("hello") | |
47 | + self.fail("Oops. Wrong argspec accepted???") | |
48 | + e = self.assertRaises(xmlrpclib.Fault, local_test) | |
49 | + self.assertEquals(1, e.faultCode) | |
50 | + self.assertTrue("listMethods() takes exactly 2 arguments" \ | |
51 | + in e.faultString) | |
52 | + | |
53 | + def test_content_encoding(self): | |
54 | + test_string = "øæåØÆÅàéüoö" | |
55 | + # No encoding / encoding error | |
56 | + def local_test(): | |
57 | + t_id = self.admin.ticket.create(test_string, test_string[::-1], {}) | |
58 | + self.admin.ticket.delete(t_id) | |
59 | + self.fail("Expected ticket create to fail...") | |
60 | + e = self.assertRaises(xmlrpclib.Fault, local_test) | |
61 | + self.assertTrue(isinstance(e, xmlrpclib.Fault)) | |
62 | + self.assertEquals(-32700, e.faultCode) | |
63 | + # Unicode version (encodable) | |
64 | + from trac.util.text import to_unicode | |
65 | + test_string = to_unicode(test_string) | |
66 | + t_id = self.admin.ticket.create(test_string, test_string[::-1], {}) | |
67 | + self.assertTrue(t_id > 0) | |
68 | + result = self.admin.ticket.get(t_id) | |
69 | + self.assertEquals(result[0], t_id) | |
70 | + self.assertEquals(result[3]['summary'], test_string) | |
71 | + self.assertEquals(result[3]['description'], test_string[::-1]) | |
72 | + self.assertEquals(unicode, type(result[3]['summary'])) | |
73 | + self.admin.ticket.delete(t_id) | |
74 | + | |
75 | + def test_to_and_from_datetime(self): | |
76 | + from datetime import datetime | |
77 | + from trac.util.datefmt import to_datetime, utc | |
78 | + from tracrpc.xml_rpc import to_xmlrpc_datetime, from_xmlrpc_datetime | |
79 | + now = to_datetime(None, utc) | |
80 | + now_timetuple = now.timetuple()[:6] | |
81 | + xmlrpc_now = to_xmlrpc_datetime(now) | |
82 | + self.assertTrue(isinstance(xmlrpc_now, xmlrpclib.DateTime), | |
83 | + "Expected xmlprc_now to be an xmlrpclib.DateTime") | |
84 | + self.assertEquals(str(xmlrpc_now), now.strftime("%Y%m%dT%H:%M:%S")) | |
85 | + now_from_xmlrpc = from_xmlrpc_datetime(xmlrpc_now) | |
86 | + self.assertTrue(isinstance(now_from_xmlrpc, datetime), | |
87 | + "Expected now_from_xmlrpc to be a datetime") | |
88 | + self.assertEquals(now_from_xmlrpc.timetuple()[:6], now_timetuple) | |
89 | + self.assertEquals(now_from_xmlrpc.tzinfo, utc) | |
90 | + | |
91 | + def test_resource_not_found(self): | |
92 | + # A Ticket resource | |
93 | + e = self.assertRaises(xmlrpclib.Fault, self.admin.ticket.get, 2147483647) | |
94 | + self.assertEquals(e.faultCode, 404) | |
95 | + self.assertEquals(e.faultString, | |
96 | + 'Ticket 2147483647 does not exist.') | |
97 | + # A Wiki resource | |
98 | + e = self.assertRaises(xmlrpclib.Fault, self.admin.wiki.getPage, "Test", 10) | |
99 | + self.assertEquals(e.faultCode, 404) | |
100 | + self.assertEquals(e.faultString, | |
101 | + 'Wiki page "Test" does not exist at version 10') | |
102 | + | |
103 | + def test_xml_encoding_special_characters(self): | |
104 | + tid1 = self.admin.ticket.create( | |
105 | + 'One & Two < Four', 'Desc & ription', {}) | |
106 | + ticket = self.admin.ticket.get(tid1) | |
107 | + self.assertEquals('One & Two < Four', ticket[3]['summary']) | |
108 | + self.assertEquals('Desc & ription', ticket[3]['description']) | |
109 | + self.admin.ticket.delete(tid1) | |
110 | + | |
111 | +def test_suite(): | |
112 | + return unittest.makeSuite(RpcXmlTestCase) | |
113 | + | |
114 | +if __name__ == '__main__': | |
115 | + unittest.main(defaultTest='test_suite') |
@@ -129,6 +129,182 @@ | ||
129 | 129 | self.admin.ticket.delete(tid1) |
130 | 130 | self.admin.ticket.delete(tid2) |
131 | 131 | |
132 | + def test_query_special_character_escape(self): | |
133 | + # Note: This test only passes when using Trac 0.12+ | |
134 | + # See http://trac-hacks.org/ticket/7737 | |
135 | + if __import__('trac').__version__ < '0.12': | |
136 | + self.fail("Known issue: Trac 0.11 does not handle escaped input properly.") | |
137 | + summary = ["here&now", "maybe|later", "back\slash"] | |
138 | + search = ["here\&now", "maybe\|later", "back\\slash"] | |
139 | + tids = [] | |
140 | + for s in summary: | |
141 | + tids.append(self.admin.ticket.create(s, | |
142 | + "test_special_character_escape", {})) | |
143 | + try: | |
144 | + for i in range(0, 3): | |
145 | + self.assertEquals([tids[i]], | |
146 | + self.admin.ticket.query("summary=%s" % search[i])) | |
147 | + self.assertEquals(tids.sort(), | |
148 | + self.admin.ticket.query("summary=%s" % "|".join(search)).sort()) | |
149 | + finally: | |
150 | + for tid in tids: | |
151 | + self.admin.ticket.delete(tid) | |
152 | + | |
153 | + def test_update_author(self): | |
154 | + tid = self.admin.ticket.create("ticket_update_author", "one", {}) | |
155 | + self.admin.ticket.update(tid, 'comment1', {}) | |
156 | + time.sleep(1) | |
157 | + self.admin.ticket.update(tid, 'comment2', {}, False, 'foo') | |
158 | + time.sleep(1) | |
159 | + self.user.ticket.update(tid, 'comment3', {}, False, 'should_be_rejected') | |
160 | + changes = self.admin.ticket.changeLog(tid) | |
161 | + self.assertEquals(3, len(changes)) | |
162 | + for when, who, what, cnum, comment, _tid in changes: | |
163 | + self.assertTrue(comment in ('comment1', 'comment2', 'comment3')) | |
164 | + if comment == 'comment1': | |
165 | + self.assertEquals('admin', who) | |
166 | + if comment == 'comment2': | |
167 | + self.assertEquals('foo', who) | |
168 | + if comment == 'comment3': | |
169 | + self.assertEquals('user', who) | |
170 | + self.admin.ticket.delete(tid) | |
171 | + | |
172 | + def test_create_at_time(self): | |
173 | + from trac.util.datefmt import to_datetime, utc | |
174 | + now = to_datetime(None, utc) | |
175 | + minus1 = now - datetime.timedelta(days=1) | |
176 | + # create the tickets (user ticket will not be permitted to change time) | |
177 | + one = self.admin.ticket.create("create_at_time1", "ok", {}, False, | |
178 | + xmlrpclib.DateTime(minus1)) | |
179 | + two = self.user.ticket.create("create_at_time3", "ok", {}, False, | |
180 | + xmlrpclib.DateTime(minus1)) | |
181 | + # get the tickets | |
182 | + t1 = self.admin.ticket.get(one) | |
183 | + t2 = self.admin.ticket.get(two) | |
184 | + # check timestamps | |
185 | + self.assertTrue(t1[1] < t2[1]) | |
186 | + self.admin.ticket.delete(one) | |
187 | + self.admin.ticket.delete(two) | |
188 | + | |
189 | + def test_update_at_time(self): | |
190 | + from trac.util.datefmt import to_datetime, utc | |
191 | + now = to_datetime(None, utc) | |
192 | + minus1 = now - datetime.timedelta(hours=1) | |
193 | + minus2 = now - datetime.timedelta(hours=2) | |
194 | + tid = self.admin.ticket.create("ticket_update_at_time", "ok", {}) | |
195 | + self.admin.ticket.update(tid, 'one', {}, False, '', xmlrpclib.DateTime(minus2)) | |
196 | + self.admin.ticket.update(tid, 'two', {}, False, '', xmlrpclib.DateTime(minus1)) | |
197 | + self.user.ticket.update(tid, 'three', {}, False, '', xmlrpclib.DateTime(minus1)) | |
198 | + time.sleep(1) | |
199 | + self.user.ticket.update(tid, 'four', {}) | |
200 | + changes = self.admin.ticket.changeLog(tid) | |
201 | + self.assertEquals(4, len(changes)) | |
202 | + # quick test to make sure each is older than previous | |
203 | + self.assertTrue(changes[0][0] < changes[1][0] < changes[2][0]) | |
204 | + # margin of 2 seconds for tests | |
205 | + justnow = xmlrpclib.DateTime(now - datetime.timedelta(seconds=1)) | |
206 | + self.assertTrue(justnow <= changes[2][0]) | |
207 | + self.assertTrue(justnow <= changes[3][0]) | |
208 | + self.admin.ticket.delete(tid) | |
209 | + | |
210 | + def test_update_non_existing(self): | |
211 | + try: | |
212 | + self.admin.ticket.update(3344, "a comment", {}) | |
213 | + self.fail("Allowed to update non-existing ticket???") | |
214 | + self.admin.ticket.delete(3234) | |
215 | + except Exception, e: | |
216 | + self.assertTrue("Ticket 3344 does not exist." in str(e)) | |
217 | + | |
218 | + def test_update_basic(self): | |
219 | + import time | |
220 | + # Basic update check, no 'action' or 'time_changed' | |
221 | + tid = self.admin.ticket.create('test_update_basic1', 'ieidnsj', { | |
222 | + 'owner': 'osimons'}) | |
223 | + # old-style (deprecated) | |
224 | + self.admin.ticket.update(tid, "comment1", {'component': 'component2'}) | |
225 | + self.assertEquals(2, len(self.admin.ticket.changeLog(tid))) | |
226 | + # new-style with 'action' | |
227 | + time.sleep(1) # avoid "columns ticket, time, field are not unique" | |
228 | + self.admin.ticket.update(tid, "comment2", {'component': 'component1', | |
229 | + 'action': 'leave'}) | |
230 | + self.assertEquals(4, len(self.admin.ticket.changeLog(tid))) | |
231 | + self.admin.ticket.delete(tid) | |
232 | + | |
233 | + def test_update_time_changed(self): | |
234 | + # Update with collision check | |
235 | + import datetime | |
236 | + from tracrpc.xml_rpc import from_xmlrpc_datetime, to_xmlrpc_datetime | |
237 | + tid = self.admin.ticket.create('test_update_time_changed', '...', {}) | |
238 | + tid, created, modified, attrs = self.admin.ticket.get(tid) | |
239 | + then = from_xmlrpc_datetime(modified) - datetime.timedelta(minutes=1) | |
240 | + # Unrestricted old-style update (to be removed soon) | |
241 | + try: | |
242 | + self.admin.ticket.update(tid, "comment1", | |
243 | + {'_ts': to_xmlrpc_datetime(then)}) | |
244 | + except Exception, e: | |
245 | + self.assertTrue("Ticket has been updated since last get" in str(e)) | |
246 | + # Update with 'action' to test new-style update. | |
247 | + try: | |
248 | + self.admin.ticket.update(tid, "comment1", | |
249 | + {'_ts': to_xmlrpc_datetime(then), | |
250 | + 'action': 'leave'}) | |
251 | + except Exception, e: | |
252 | + self.assertTrue("modified by someone else" in str(e)) | |
253 | + self.admin.ticket.delete(tid) | |
254 | + | |
255 | + def test_update_time_same(self): | |
256 | + # Update with collision check | |
257 | + import datetime | |
258 | + from tracrpc.xml_rpc import from_xmlrpc_datetime, to_xmlrpc_datetime | |
259 | + | |
260 | + # Unrestricted old-style update (to be removed soon) | |
261 | + tid = self.admin.ticket.create('test_update_time_same', '...', {}) | |
262 | + tid, created, modified, attrs = self.admin.ticket.get(tid) | |
263 | + ts = attrs['_ts'] | |
264 | + self.admin.ticket.update(tid, "comment1", | |
265 | + {'_ts': ts}) | |
266 | + self.admin.ticket.delete(tid) | |
267 | + | |
268 | + # Update with 'action' to test new-style update. | |
269 | + tid = self.admin.ticket.create('test_update_time_same', '...', {}) | |
270 | + tid, created, modified, attrs = self.admin.ticket.get(tid) | |
271 | + ts = attrs['_ts'] | |
272 | + self.admin.ticket.update(tid, "comment1", | |
273 | + {'_ts': ts, 'action': 'leave'}) | |
274 | + self.admin.ticket.delete(tid) | |
275 | + | |
276 | + def test_update_action(self): | |
277 | + # Updating with 'action' in attributes | |
278 | + tid = self.admin.ticket.create('test_update_action', 'ss') | |
279 | + current = self.admin.ticket.get(tid) | |
280 | + self.assertEqual('', current[3].get('owner', '')) | |
281 | + updated = self.admin.ticket.update(tid, "comment1", | |
282 | + {'action': 'reassign', | |
283 | + 'action_reassign_reassign_owner': 'user'}) | |
284 | + self.assertEqual('user', updated[3].get('owner')) | |
285 | + self.admin.ticket.delete(tid) | |
286 | + | |
287 | + def test_update_action_non_existing(self): | |
288 | + # Updating with non-existing 'action' in attributes | |
289 | + tid = self.admin.ticket.create('test_update_action_wrong', 'ss') | |
290 | + try: | |
291 | + self.admin.ticket.update(tid, "comment1", | |
292 | + {'action': 'reassign', | |
293 | + 'action_reassign_reassign_owner': 'user'}) | |
294 | + except Exception, e: | |
295 | + self.assertTrue("invalid action" in str(e)) | |
296 | + self.admin.ticket.delete(tid) | |
297 | + | |
298 | + def test_update_field_non_existing(self): | |
299 | + tid = self.admin.ticket.create('test_update_field_non_existing', 'yw3') | |
300 | + try: | |
301 | + self.admin.ticket.update(tid, "comment1", | |
302 | + {'does_not_exist': 'eiwrjoer'}) | |
303 | + except Exception, e: | |
304 | + self.assertTrue("no such column" in str(e)) | |
305 | + self.admin.ticket.delete(tid) | |
306 | + | |
307 | + | |
132 | 308 | class RpcTicketVersionTestCase(TracRpcTestCase): |
133 | 309 | |
134 | 310 | def setUp(self): |
@@ -64,6 +64,27 @@ | ||
64 | 64 | self.admin.wiki.deletePage('WikiOne') |
65 | 65 | self.admin.wiki.deletePage('WikiTwo') |
66 | 66 | |
67 | + def test_getPageHTMLWithImage(self): | |
68 | + # Create the wiki page (absolute image reference) | |
69 | + self.admin.wiki.putPage('ImageTest', | |
70 | + '[[Image(wiki:ImageTest:feed.png, nolink)]]\n', {}) | |
71 | + # Create attachment | |
72 | + image_url = os.path.join(rpc_testenv.trac_src, 'trac', | |
73 | + 'htdocs', 'feed.png') | |
74 | + self.admin.wiki.putAttachmentEx('ImageTest', 'feed.png', 'test image', | |
75 | + xmlrpclib.Binary(open(image_url, 'r').read())) | |
76 | + # Check rendering absolute | |
77 | + markup_1 = self.admin.wiki.getPageHTML('ImageTest') | |
78 | + self.assertEquals('<html><body><p>\n<img src="http://127.0.0.1:8765' | |
79 | + '/raw-attachment/wiki/ImageTest/feed.png" alt="test image" ' | |
80 | + 'title="test image" />\n</p>\n</body></html>', markup_1) | |
81 | + # Change to relative image reference and check again | |
82 | + self.admin.wiki.putPage('ImageTest', | |
83 | + '[[Image(feed.png, nolink)]]\n', {}) | |
84 | + markup_2 = self.admin.wiki.getPageHTML('ImageTest') | |
85 | + self.assertEquals(markup_2, markup_1) | |
86 | + | |
87 | + | |
67 | 88 | def test_suite(): |
68 | 89 | return unittest.makeSuite(RpcWikiTestCase) |
69 | 90 |
@@ -100,6 +100,14 @@ | ||
100 | 100 | self.assertEquals(e.faultString, |
101 | 101 | 'Wiki page "Test" does not exist at version 10') |
102 | 102 | |
103 | + def test_xml_encoding_special_characters(self): | |
104 | + tid1 = self.admin.ticket.create( | |
105 | + 'One & Two < Four', 'Desc & ription', {}) | |
106 | + ticket = self.admin.ticket.get(tid1) | |
107 | + self.assertEquals('One & Two < Four', ticket[3]['summary']) | |
108 | + self.assertEquals('Desc & ription', ticket[3]['description']) | |
109 | + self.admin.ticket.delete(tid1) | |
110 | + | |
103 | 111 | def test_suite(): |
104 | 112 | return unittest.makeSuite(RpcXmlTestCase) |
105 | 113 |
@@ -43,8 +43,17 @@ | ||
43 | 43 | yield (None, ((list, int),), self.getAvailableActions) |
44 | 44 | yield (None, ((list, int),), self.getActions) |
45 | 45 | yield (None, ((list, int),), self.get) |
46 | - yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create) | |
47 | - yield (None, ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update) | |
46 | + yield ('TICKET_CREATE', ((int, str, str), | |
47 | + (int, str, str, dict), | |
48 | + (int, str, str, dict, bool), | |
49 | + (int, str, str, dict, bool, datetime)), | |
50 | + self.create) | |
51 | + yield (None, ((list, int, str), | |
52 | + (list, int, str, dict), | |
53 | + (list, int, str, dict, bool), | |
54 | + (list, int, str, dict, bool, str), | |
55 | + (list, int, str, dict, bool, str, datetime)), | |
56 | + self.update) | |
48 | 57 | yield (None, ((None, int),), self.delete) |
49 | 58 | yield (None, ((dict, int), (dict, int, int)), self.changeLog) |
50 | 59 | yield (None, ((list, int),), self.listAttachments) |
@@ -58,7 +67,13 @@ | ||
58 | 67 | |
59 | 68 | # Exported methods |
60 | 69 | def query(self, req, qstr='status!=closed'): |
61 | - """ Perform a ticket query, returning a list of ticket ID's. """ | |
70 | + """ | |
71 | + Perform a ticket query, returning a list of ticket ID's. | |
72 | + All queries will use stored settings for maximum number of results per | |
73 | + page and paging options. Use `max=n` to define number of results to | |
74 | + receive, and use `page=n` to page through larger result sets. Using | |
75 | + `max=0` will turn off paging and return all results. | |
76 | + """ | |
62 | 77 | q = query.Query.from_string(self.env, qstr) |
63 | 78 | ticket_realm = Resource('ticket') |
64 | 79 | out = [] |
@@ -96,7 +111,7 @@ | ||
96 | 111 | t = model.Ticket(self.env, id) |
97 | 112 | actions = [] |
98 | 113 | for action in ts.get_available_actions(req, t): |
99 | - fragment = hints = genshi.builder.Fragment() | |
114 | + fragment = genshi.builder.Fragment() | |
100 | 115 | hints = [] |
101 | 116 | first_label = None |
102 | 117 | for controller in ts.action_controllers: |
@@ -133,10 +148,12 @@ | ||
133 | 148 | """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ |
134 | 149 | t = model.Ticket(self.env, id) |
135 | 150 | req.perm(t.resource).require('TICKET_VIEW') |
151 | + t['_ts'] = str(t.time_changed) | |
136 | 152 | return (t.id, t.time_created, t.time_changed, t.values) |
137 | 153 | |
138 | - def create(self, req, summary, description, attributes = {}, notify=False): | |
139 | - """ Create a new ticket, returning the ticket ID. """ | |
154 | + def create(self, req, summary, description, attributes={}, notify=False, when=None): | |
155 | + """ Create a new ticket, returning the ticket ID. | |
156 | + Overriding 'when' requires admin permission. """ | |
140 | 157 | t = model.Ticket(self.env) |
141 | 158 | t['summary'] = summary |
142 | 159 | t['description'] = description |
@@ -145,7 +162,12 @@ | ||
145 | 162 | t[k] = v |
146 | 163 | t['status'] = 'new' |
147 | 164 | t['resolution'] = '' |
148 | - t.insert() | |
165 | + # custom create timestamp? | |
166 | + if when and not 'TICKET_ADMIN' in req.perm: | |
167 | + self.log.warn("RPC ticket.create: %r not allowed to create with " | |
168 | + "non-current timestamp (%r)", req.authname, when) | |
169 | + when = None | |
170 | + t.insert(when=when) | |
149 | 171 | # Call ticket change listeners |
150 | 172 | ts = TicketSystem(self.env) |
151 | 173 | for listener in ts.change_listeners: |
@@ -159,22 +181,47 @@ | ||
159 | 181 | "of ticket #%s: %s" % (t.id, e)) |
160 | 182 | return t.id |
161 | 183 | |
162 | - def update(self, req, id, comment, attributes = {}, notify=False): | |
184 | + def update(self, req, id, comment, attributes={}, notify=False, author='', when=None): | |
163 | 185 | """ Update a ticket, returning the new ticket in the same form as |
164 | - getTicket(). Requires a valid 'action' in attributes to support workflow. """ | |
165 | - now = to_datetime(None, utc) | |
186 | + get(). 'New-style' call requires two additional items in attributes: | |
187 | + (1) 'action' for workflow support (including any supporting fields | |
188 | + as retrieved by getActions()), | |
189 | + (2) '_ts' changetime token for detecting update collisions (as received | |
190 | + from get() or update() calls). | |
191 | + ''Calling update without 'action' and '_ts' changetime token is | |
192 | + deprecated, and will raise errors in a future version.'' """ | |
166 | 193 | t = model.Ticket(self.env, id) |
194 | + # custom author? | |
195 | + if author and not (req.authname == 'anonymous' \ | |
196 | + or 'TICKET_ADMIN' in req.perm(t.resource)): | |
197 | + # only allow custom author if anonymous is permitted or user is admin | |
198 | + self.log.warn("RPC ticket.update: %r not allowed to change author " | |
199 | + "to %r for comment on #%d", req.authname, author, id) | |
200 | + author = '' | |
201 | + author = author or req.authname | |
202 | + # custom change timestamp? | |
203 | + if when and not 'TICKET_ADMIN' in req.perm(t.resource): | |
204 | + self.log.warn("RPC ticket.update: %r not allowed to update #%d with " | |
205 | + "non-current timestamp (%r)", author, id, when) | |
206 | + when = None | |
207 | + when = when or to_datetime(None, utc) | |
208 | + # and action... | |
167 | 209 | if not 'action' in attributes: |
168 | 210 | # FIXME: Old, non-restricted update - remove soon! |
169 | 211 | self.log.warning("Rpc ticket.update for ticket %d by user %s " \ |
170 | 212 | "has no workflow 'action'." % (id, req.authname)) |
171 | 213 | req.perm(t.resource).require('TICKET_MODIFY') |
214 | + time_changed = attributes.pop('_ts', None) | |
215 | + if time_changed and str(time_changed) != str(t.time_changed): | |
216 | + raise TracError("Ticket has been updated since last get().") | |
172 | 217 | for k, v in attributes.iteritems(): |
173 | 218 | t[k] = v |
174 | - t.save_changes(req.authname, comment, when=now) | |
219 | + t.save_changes(author, comment, when=when) | |
175 | 220 | else: |
176 | 221 | ts = TicketSystem(self.env) |
177 | 222 | tm = TicketModule(self.env) |
223 | + # TODO: Deprecate update without time_changed timestamp | |
224 | + time_changed = str(attributes.pop('_ts', t.time_changed)) | |
178 | 225 | action = attributes.get('action') |
179 | 226 | avail_actions = ts.get_available_actions(req, t) |
180 | 227 | if not action in avail_actions: |
@@ -188,7 +235,7 @@ | ||
188 | 235 | # TicketModule reads req.args - need to move things there... |
189 | 236 | req.args.update(attributes) |
190 | 237 | req.args['comment'] = comment |
191 | - req.args['ts'] = str(t.time_changed) # collision hack... | |
238 | + req.args['ts'] = time_changed | |
192 | 239 | changes, problems = tm.get_ticket_changes(req, t, action) |
193 | 240 | for warning in problems: |
194 | 241 | add_warning(req, "Rpc ticket.update: %s" % warning) |
@@ -199,17 +246,17 @@ | ||
199 | 246 | else: |
200 | 247 | tm._apply_ticket_changes(t, changes) |
201 | 248 | self.log.debug("Rpc ticket.update save: %s" % repr(t.values)) |
202 | - t.save_changes(req.authname, comment, when=now) | |
249 | + t.save_changes(author, comment, when=when) | |
203 | 250 | # Apply workflow side-effects |
204 | 251 | for controller in controllers: |
205 | 252 | controller.apply_action_side_effects(req, t, action) |
206 | 253 | # Call ticket change listeners |
207 | 254 | for listener in ts.change_listeners: |
208 | - listener.ticket_changed(t, comment, req.authname, t._old) | |
255 | + listener.ticket_changed(t, comment, author, t._old) | |
209 | 256 | if notify: |
210 | 257 | try: |
211 | 258 | tn = TicketNotifyEmail(self.env) |
212 | - tn.notify(t, newticket=False, modtime=now) | |
259 | + tn.notify(t, newticket=False, modtime=when) | |
213 | 260 | except Exception, e: |
214 | 261 | self.log.exception("Failure sending notification on change of " |
215 | 262 | "ticket #%s: %s" % (t.id, e)) |
@@ -204,4 +204,5 @@ | ||
204 | 204 | def get_navigation_items(self, req): |
205 | 205 | if req.perm.has_permission('XML_RPC'): |
206 | 206 | yield ('metanav', 'rpc', |
207 | - tag.a('RPC API', href=req.href.rpc(), accesskey=1)) | |
207 | + tag.a('API', href=req.href.rpc(), accesskey=1)) | |
208 | + |
@@ -11,10 +11,11 @@ | ||
11 | 11 | |
12 | 12 | from trac.attachment import Attachment |
13 | 13 | from trac.core import * |
14 | +from trac.mimeview import Context | |
14 | 15 | from trac.resource import Resource, ResourceNotFound |
15 | 16 | from trac.wiki.api import WikiSystem |
16 | 17 | from trac.wiki.model import WikiPage |
17 | -from trac.wiki.formatter import wiki_to_html | |
18 | +from trac.wiki.formatter import wiki_to_html, format_to_html | |
18 | 19 | |
19 | 20 | from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary |
20 | 21 | from tracrpc.util import StringIO, to_utimestamp, from_utimestamp |
@@ -94,7 +95,9 @@ | ||
94 | 95 | def getPageHTML(self, req, pagename, version=None): |
95 | 96 | """ Return page in rendered HTML, latest version. """ |
96 | 97 | text = self.getPage(req, pagename, version) |
97 | - html = wiki_to_html(text, self.env, req, absurls=1) | |
98 | + resource = Resource('wiki', pagename, version) | |
99 | + context = Context.from_request(req, resource, absurls=True) | |
100 | + html = format_to_html(self.env, context, text) | |
98 | 101 | return '<html><body>%s</body></html>' % html |
99 | 102 | |
100 | 103 | def getAllPages(self, req): |
@@ -17,6 +17,7 @@ | ||
17 | 17 | from trac.resource import ResourceNotFound |
18 | 18 | from trac.util.datefmt import utc |
19 | 19 | from trac.util.text import to_unicode |
20 | +from trac.web.api import RequestDone | |
20 | 21 | |
21 | 22 | from tracrpc.api import XMLRPCSystem, IRPCProtocol, Binary, \ |
22 | 23 | RPCError, MethodNotFound, ProtocolException, ServiceException |
@@ -146,6 +147,7 @@ | ||
146 | 147 | req.send_header('Content-Length', len(response)) |
147 | 148 | req.end_headers() |
148 | 149 | req.write(response) |
150 | + raise RequestDone | |
149 | 151 | |
150 | 152 | def _normalize_xml_input(self, args): |
151 | 153 | """ Normalizes arguments (at any level - traversing dicts and lists): |