1 /*
2 * log.c: mod_dav_svn REPORT handler for querying revision log info
3 *
4 * ====================================================================
5 * Licensed to the Apache Software Foundation (ASF) under one
6 * or more contributor license agreements. See the NOTICE file
7 * distributed with this work for additional information
8 * regarding copyright ownership. The ASF licenses this file
9 * to you under the Apache License, Version 2.0 (the
10 * "License"); you may not use this file except in compliance
11 * with the License. You may obtain a copy of the License at
12 *
13 * http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing,
16 * software distributed under the License is distributed on an
17 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 * KIND, either express or implied. See the License for the
19 * specific language governing permissions and limitations
20 * under the License.
21 * ====================================================================
22 */
23
24 #include <apr_pools.h>
25 #include <apr_strings.h>
26 #include <apr_xml.h>
27
28 #include <mod_dav.h>
29
30 #include "svn_repos.h"
31 #include "svn_string.h"
32 #include "svn_types.h"
33 #include "svn_base64.h"
34 #include "svn_xml.h"
35 #include "svn_path.h"
36 #include "svn_dav.h"
37 #include "svn_pools.h"
38 #include "svn_props.h"
39
40 #include "private/svn_log.h"
41 #include "private/svn_fspath.h"
42
43 #include "../dav_svn.h"
44
45
46 struct log_receiver_baton
47 {
48 /* this buffers the output for a bit and is automatically flushed,
49 at appropriate times, by the Apache filter system. */
50 apr_bucket_brigade *bb;
51
52 /* where to deliver the output */
53 dav_svn__output *output;
54
55 /* Whether we've written the <S:log-report> header. Allows for lazy
56 writes to support mod_dav-based error handling. */
57 svn_boolean_t needs_header;
58
59 /* Whether we've written the <S:log-item> header for the current revision.
60 Allows for lazy XML node creation while receiving the data through
61 callbacks. */
62 svn_boolean_t needs_log_item;
63
64 /* How deep we are in the log message tree. We only need to surpress the
65 SVN_INVALID_REVNUM message if the stack_depth is 0. */
66 int stack_depth;
67
68 /* whether the client requested any custom revprops */
69 svn_boolean_t requested_custom_revprops;
70
71 /* whether the client can handle encoded binary property values */
72 svn_boolean_t encode_binary_props;
73
74 /* Helper variables to force early bucket brigade flushes */
75 int result_count;
76 int next_forced_flush;
77 };
78
79
80 /* If LRB->needs_header is true, send the "<S:log-report>" start
81 element and set LRB->needs_header to zero. Else do nothing.
82 This is basically duplicated in file_revs.c. Consider factoring if
83 duplicating again. */
84 static svn_error_t *
maybe_send_header(struct log_receiver_baton * lrb)85 maybe_send_header(struct log_receiver_baton *lrb)
86 {
87 if (lrb->needs_header)
88 {
89 SVN_ERR(dav_svn__brigade_puts(lrb->bb, lrb->output,
90 DAV_XML_HEADER DEBUG_CR
91 "<S:log-report xmlns:S=\""
92 SVN_XML_NAMESPACE "\" "
93 "xmlns:D=\"DAV:\">" DEBUG_CR));
94 lrb->needs_header = FALSE;
95 }
96
97 return SVN_NO_ERROR;
98 }
99
100 /* If LRB->needs_log_item is true, send the "<S:log-item>" start
101 element and set LRB->needs_log_item to zero. Else do nothing. */
102 static svn_error_t *
maybe_start_log_item(struct log_receiver_baton * lrb)103 maybe_start_log_item(struct log_receiver_baton *lrb)
104 {
105 if (lrb->needs_log_item)
106 {
107 SVN_ERR(dav_svn__brigade_printf(lrb->bb, lrb->output,
108 "<S:log-item>" DEBUG_CR));
109 lrb->needs_log_item = FALSE;
110 }
111
112 return SVN_NO_ERROR;
113 }
114
115 /* Utility for log_receiver opening a new XML element in LRB's brigade
116 for LOG_ITEM and return the element's name in *ELEMENT. Use POOL for
117 temporary allocations.
118
119 Call this function for items that may have a copy-from */
120 static svn_error_t *
start_path_with_copy_from(const char ** element,struct log_receiver_baton * lrb,svn_repos_path_change_t * log_item,apr_pool_t * pool)121 start_path_with_copy_from(const char **element,
122 struct log_receiver_baton *lrb,
123 svn_repos_path_change_t *log_item,
124 apr_pool_t *pool)
125 {
126 switch (log_item->change_kind)
127 {
128 case svn_fs_path_change_add:
129 *element = "S:added-path";
130 break;
131
132 case svn_fs_path_change_replace:
133 *element = "S:replaced-path";
134 break;
135
136 default:
137 /* Caller, you did wrong! */
138 SVN_ERR_MALFUNCTION();
139 }
140
141 if (log_item->copyfrom_path
142 && SVN_IS_VALID_REVNUM(log_item->copyfrom_rev))
143 SVN_ERR(dav_svn__brigade_printf
144 (lrb->bb, lrb->output,
145 "<%s copyfrom-path=\"%s\" copyfrom-rev=\"%ld\"",
146 *element,
147 apr_xml_quote_string(pool,
148 log_item->copyfrom_path,
149 1), /* escape quotes */
150 log_item->copyfrom_rev));
151 else
152 SVN_ERR(dav_svn__brigade_printf(lrb->bb, lrb->output, "<%s", *element));
153
154 return SVN_NO_ERROR;
155 }
156
157
158 /* This implements `svn_repos_path_change_receiver_t'.
159 BATON is a `struct log_receiver_baton *'. */
160 static svn_error_t *
log_change_receiver(void * baton,svn_repos_path_change_t * change,apr_pool_t * scratch_pool)161 log_change_receiver(void *baton,
162 svn_repos_path_change_t *change,
163 apr_pool_t *scratch_pool)
164 {
165 struct log_receiver_baton *lrb = baton;
166 const char *close_element = NULL;
167
168 /* We must open the XML nodes for the report and log-item before
169 sending the first changed path.
170
171 Note that we can't get here for empty revisions that log() injects
172 to indicate the end of a recursive merged rev sequence.
173 */
174 SVN_ERR(maybe_send_header(lrb));
175 SVN_ERR(maybe_start_log_item(lrb));
176
177 /* ### todo: is there a D: namespace equivalent for
178 `changed-path'? Should use it if so. */
179 switch (change->change_kind)
180 {
181 case svn_fs_path_change_add:
182 case svn_fs_path_change_replace:
183 SVN_ERR(start_path_with_copy_from(&close_element, lrb,
184 change, scratch_pool));
185 break;
186
187 case svn_fs_path_change_delete:
188 SVN_ERR(dav_svn__brigade_puts(lrb->bb, lrb->output,
189 "<S:deleted-path"));
190 close_element = "S:deleted-path";
191 break;
192
193 case svn_fs_path_change_modify:
194 SVN_ERR(dav_svn__brigade_puts(lrb->bb, lrb->output,
195 "<S:modified-path"));
196 close_element = "S:modified-path";
197 break;
198
199 default:
200 break;
201 }
202
203 /* If we need to close the element, then send the attributes
204 that apply to all changed items and then close the element. */
205 if (close_element)
206 SVN_ERR(dav_svn__brigade_printf
207 (lrb->bb, lrb->output,
208 " node-kind=\"%s\""
209 " text-mods=\"%s\""
210 " prop-mods=\"%s\">%s</%s>" DEBUG_CR,
211 svn_node_kind_to_word(change->node_kind),
212 change->text_mod ? "true" : "false",
213 change->prop_mod ? "true" : "false",
214 apr_xml_quote_string(scratch_pool, change->path.data, 0),
215 close_element));
216
217 return SVN_NO_ERROR;
218 }
219
220 /* This implements `svn_repos_log_entry_receiver_t'.
221 BATON is a `struct log_receiver_baton *'. */
222 static svn_error_t *
log_revision_receiver(void * baton,svn_repos_log_entry_t * log_entry,apr_pool_t * scratch_pool)223 log_revision_receiver(void *baton,
224 svn_repos_log_entry_t *log_entry,
225 apr_pool_t *scratch_pool)
226 {
227 struct log_receiver_baton *lrb = baton;
228
229 SVN_ERR(maybe_send_header(lrb));
230
231 if (log_entry->revision == SVN_INVALID_REVNUM)
232 {
233 /* If the stack depth is zero, we've seen the last revision, so don't
234 send it, just return. The footer will be sent later. */
235 if (lrb->stack_depth == 0)
236 return SVN_NO_ERROR;
237 else
238 lrb->stack_depth--;
239 }
240
241 /* If we have not received any path changes, the log-item XML node
242 still needs to be opened. Also, reset the controlling flag to
243 prepare it for the next revision - if there should be one. */
244 SVN_ERR(maybe_start_log_item(lrb));
245 lrb->needs_log_item = TRUE;
246
247 /* Path changes have been processed already.
248 Now send the remaining per-revision info. */
249 SVN_ERR(dav_svn__brigade_printf(lrb->bb, lrb->output,
250 "<D:version-name>%ld"
251 "</D:version-name>" DEBUG_CR,
252 log_entry->revision));
253
254 if (log_entry->revprops)
255 {
256 apr_pool_t *iterpool = svn_pool_create(scratch_pool);
257 apr_hash_index_t *hi;
258 for (hi = apr_hash_first(scratch_pool, log_entry->revprops);
259 hi != NULL;
260 hi = apr_hash_next(hi))
261 {
262 char *name;
263 void *val;
264 const svn_string_t *value;
265 const char *encoding_str = "";
266
267 svn_pool_clear(iterpool);
268 apr_hash_this(hi, (void *)&name, NULL, &val);
269 value = val;
270
271 /* If the client is okay with us encoding binary (or really,
272 any non-XML-safe) property values, do so as necessary. */
273 if (lrb->encode_binary_props)
274 {
275 if (! svn_xml_is_xml_safe(value->data, value->len))
276 {
277 value = svn_base64_encode_string2(value, TRUE, iterpool);
278 encoding_str = " encoding=\"base64\"";
279 }
280 }
281
282 if (strcmp(name, SVN_PROP_REVISION_AUTHOR) == 0)
283 SVN_ERR(dav_svn__brigade_printf
284 (lrb->bb, lrb->output,
285 "<D:creator-displayname%s>%s</D:creator-displayname>"
286 DEBUG_CR, encoding_str,
287 apr_xml_quote_string(iterpool, value->data, 0)));
288 else if (strcmp(name, SVN_PROP_REVISION_DATE) == 0)
289 /* ### this should be DAV:creation-date, but we need to format
290 ### that date a bit differently */
291 SVN_ERR(dav_svn__brigade_printf
292 (lrb->bb, lrb->output,
293 "<S:date%s>%s</S:date>" DEBUG_CR, encoding_str,
294 apr_xml_quote_string(iterpool, value->data, 0)));
295 else if (strcmp(name, SVN_PROP_REVISION_LOG) == 0)
296 SVN_ERR(dav_svn__brigade_printf
297 (lrb->bb, lrb->output,
298 "<D:comment%s>%s</D:comment>" DEBUG_CR, encoding_str,
299 apr_xml_quote_string(scratch_pool,
300 svn_xml_fuzzy_escape(value->data,
301 iterpool), 0)));
302 else
303 SVN_ERR(dav_svn__brigade_printf
304 (lrb->bb, lrb->output,
305 "<S:revprop name=\"%s\"%s>%s</S:revprop>" DEBUG_CR,
306 apr_xml_quote_string(iterpool, name, 0), encoding_str,
307 apr_xml_quote_string(iterpool, value->data, 0)));
308 }
309
310 svn_pool_destroy(iterpool);
311 }
312
313 if (log_entry->has_children)
314 {
315 SVN_ERR(dav_svn__brigade_puts(lrb->bb, lrb->output, "<S:has-children/>"));
316 lrb->stack_depth++;
317 }
318
319 if (log_entry->subtractive_merge)
320 SVN_ERR(dav_svn__brigade_puts(lrb->bb, lrb->output,
321 "<S:subtractive-merge/>"));
322
323 SVN_ERR(dav_svn__brigade_puts(lrb->bb, lrb->output,
324 "</S:log-item>" DEBUG_CR));
325
326 /* In general APR will flush the brigade every 8000 bytes through the filter
327 stack, but log items may not be generated that fast, especially in
328 combination with authz and busy servers. We now explictly flush after
329 log-item 4, 16, 64 and 256 to produce a few results fast.
330
331 This introduces 4 full flushes of our brigade and the installed output
332 filters at growing intervals and then falls back to the standard
333 buffering of 8000 bytes + whatever buffers are added in output filters. */
334 lrb->result_count++;
335 if (lrb->result_count == lrb->next_forced_flush)
336 {
337 apr_bucket *bkt;
338
339 /* Compared to using ap_filter_flush(), which we use in other place
340 this adds a flush frame before flushing the brigade, to make output
341 filters perform a flush as well */
342
343 /* No brigade empty check. We want output filters to flush anyway */
344 bkt = apr_bucket_flush_create(
345 dav_svn__output_get_bucket_alloc(lrb->output));
346 APR_BRIGADE_INSERT_TAIL(lrb->bb, bkt);
347 SVN_ERR(dav_svn__output_pass_brigade(lrb->output, lrb->bb));
348
349 if (lrb->result_count < 256)
350 lrb->next_forced_flush = lrb->next_forced_flush * 4;
351 }
352
353 return SVN_NO_ERROR;
354 }
355
356
357 dav_error *
dav_svn__log_report(const dav_resource * resource,const apr_xml_doc * doc,dav_svn__output * output)358 dav_svn__log_report(const dav_resource *resource,
359 const apr_xml_doc *doc,
360 dav_svn__output *output)
361 {
362 svn_error_t *serr;
363 dav_error *derr = NULL;
364 apr_xml_elem *child;
365 struct log_receiver_baton lrb;
366 dav_svn__authz_read_baton arb;
367 const dav_svn_repos *repos = resource->info->repos;
368 const char *target = NULL;
369 int limit = 0;
370 int ns;
371 svn_boolean_t seen_revprop_element;
372
373 /* These get determined from the request document. */
374 svn_revnum_t start = SVN_INVALID_REVNUM; /* defaults to HEAD */
375 svn_revnum_t end = SVN_INVALID_REVNUM; /* defaults to HEAD */
376 svn_boolean_t discover_changed_paths = FALSE; /* off by default */
377 svn_boolean_t strict_node_history = FALSE; /* off by default */
378 svn_boolean_t include_merged_revisions = FALSE; /* off by default */
379
380 apr_array_header_t *revprops = apr_array_make(resource->pool, 3,
381 sizeof(const char *));
382 apr_array_header_t *paths
383 = apr_array_make(resource->pool, 1, sizeof(const char *));
384
385 /* Sanity check. */
386 if (!resource->info->repos_path)
387 return dav_svn__new_error(resource->pool, HTTP_BAD_REQUEST, 0, 0,
388 "The request does not specify a repository path");
389 ns = dav_svn__find_ns(doc->namespaces, SVN_XML_NAMESPACE);
390 if (ns == -1)
391 {
392 return dav_svn__new_error_svn(resource->pool, HTTP_BAD_REQUEST, 0, 0,
393 "The request does not contain the 'svn:' "
394 "namespace, so it is not going to have "
395 "certain required elements");
396 }
397
398 /* If this is still FALSE after the loop, we haven't seen either of
399 the revprop elements, meaning a pre-1.5 client; we'll return the
400 standard author/date/log revprops. */
401 seen_revprop_element = FALSE;
402
403 lrb.requested_custom_revprops = FALSE;
404 lrb.encode_binary_props = FALSE;
405 for (child = doc->root->first_child; child != NULL; child = child->next)
406 {
407 /* if this element isn't one of ours, then skip it */
408 if (child->ns != ns)
409 continue;
410
411 if (strcmp(child->name, "start-revision") == 0)
412 start = SVN_STR_TO_REV(dav_xml_get_cdata(child, resource->pool, 1));
413 else if (strcmp(child->name, "end-revision") == 0)
414 end = SVN_STR_TO_REV(dav_xml_get_cdata(child, resource->pool, 1));
415 else if (strcmp(child->name, "limit") == 0)
416 {
417 serr = svn_cstring_atoi(&limit,
418 dav_xml_get_cdata(child, resource->pool, 1));
419 if (serr)
420 {
421 return dav_svn__convert_err(serr, HTTP_BAD_REQUEST,
422 "Malformed CDATA in element "
423 "\"limit\"", resource->pool);
424 }
425 }
426 else if (strcmp(child->name, "discover-changed-paths") == 0)
427 discover_changed_paths = TRUE; /* presence indicates positivity */
428 else if (strcmp(child->name, "strict-node-history") == 0)
429 strict_node_history = TRUE; /* presence indicates positivity */
430 else if (strcmp(child->name, "include-merged-revisions") == 0)
431 include_merged_revisions = TRUE; /* presence indicates positivity */
432 else if (strcmp(child->name, "encode-binary-props") == 0)
433 lrb.encode_binary_props = TRUE; /* presence indicates positivity */
434 else if (strcmp(child->name, "all-revprops") == 0)
435 {
436 revprops = NULL; /* presence indicates fetch all revprops */
437 seen_revprop_element = lrb.requested_custom_revprops = TRUE;
438 }
439 else if (strcmp(child->name, "no-revprops") == 0)
440 {
441 /* presence indicates fetch no revprops */
442
443 seen_revprop_element = lrb.requested_custom_revprops = TRUE;
444 }
445 else if (strcmp(child->name, "revprop") == 0)
446 {
447 if (revprops)
448 {
449 /* We're not fetching all revprops, append to fetch list. */
450 const char *name = dav_xml_get_cdata(child, resource->pool, 0);
451 APR_ARRAY_PUSH(revprops, const char *) = name;
452 if (!lrb.requested_custom_revprops
453 && strcmp(name, SVN_PROP_REVISION_AUTHOR) != 0
454 && strcmp(name, SVN_PROP_REVISION_DATE) != 0
455 && strcmp(name, SVN_PROP_REVISION_LOG) != 0)
456 lrb.requested_custom_revprops = TRUE;
457 }
458 seen_revprop_element = TRUE;
459 }
460 else if (strcmp(child->name, "path") == 0)
461 {
462 const char *rel_path = dav_xml_get_cdata(child, resource->pool, 0);
463 if ((derr = dav_svn__test_canonical(rel_path, resource->pool)))
464 return derr;
465
466 /* Force REL_PATH to be a relative path, not an fspath. */
467 rel_path = svn_relpath_canonicalize(rel_path, resource->pool);
468
469 /* Append the REL_PATH to the base FS path to get an
470 absolute repository path. */
471 target = svn_fspath__join(resource->info->repos_path, rel_path,
472 resource->pool);
473 APR_ARRAY_PUSH(paths, const char *) = target;
474 }
475 /* else unknown element; skip it */
476 }
477
478 if (!seen_revprop_element)
479 {
480 /* pre-1.5 client */
481 APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_AUTHOR;
482 APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_DATE;
483 APR_ARRAY_PUSH(revprops, const char *) = SVN_PROP_REVISION_LOG;
484 }
485
486 /* Build authz read baton */
487 arb.r = resource->info->r;
488 arb.repos = resource->info->repos;
489
490 /* Build log receiver baton */
491 lrb.bb = apr_brigade_create(resource->pool, /* not the subpool! */
492 dav_svn__output_get_bucket_alloc(output));
493 lrb.output = output;
494 lrb.needs_header = TRUE;
495 lrb.needs_log_item = TRUE;
496 lrb.stack_depth = 0;
497 /* lrb.requested_custom_revprops set above */
498
499 lrb.result_count = 0;
500 lrb.next_forced_flush = 4;
501
502 /* Our svn_log_entry_receiver_t sends the <S:log-report> header in
503 a lazy fashion. Before writing the first log message, it assures
504 that the header has already been sent (checking the needs_header
505 flag in our log_receiver_baton structure). */
506
507 /* Send zero or more log items. */
508 serr = svn_repos_get_logs5(repos->repos,
509 paths,
510 start,
511 end,
512 limit,
513 strict_node_history,
514 include_merged_revisions,
515 revprops,
516 dav_svn__authz_read_func(&arb),
517 &arb,
518 discover_changed_paths ? log_change_receiver
519 : NULL,
520 &lrb,
521 log_revision_receiver,
522 &lrb,
523 resource->pool);
524 if (serr)
525 {
526 derr = dav_svn__convert_err(serr, HTTP_BAD_REQUEST, NULL,
527 resource->pool);
528 goto cleanup;
529 }
530
531 if ((serr = maybe_send_header(&lrb)))
532 {
533 derr = dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
534 "Error beginning REPORT response.",
535 resource->pool);
536 goto cleanup;
537 }
538
539 if ((serr = dav_svn__brigade_puts(lrb.bb, lrb.output,
540 "</S:log-report>" DEBUG_CR)))
541 {
542 derr = dav_svn__convert_err(serr, HTTP_INTERNAL_SERVER_ERROR,
543 "Error ending REPORT response.",
544 resource->pool);
545 goto cleanup;
546 }
547
548 cleanup:
549
550 dav_svn__operational_log(resource->info,
551 svn_log__log(paths, start, end, limit,
552 discover_changed_paths,
553 strict_node_history,
554 include_merged_revisions, revprops,
555 resource->pool));
556
557 return dav_svn__final_flush_or_error(resource->info->r, lrb.bb, output,
558 derr, resource->pool);
559 }
560