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