1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include "XMLRedlineExport.hxx"
21 #include <o3tl/any.hxx>
22 #include <tools/debug.hxx>
23 #include <rtl/ustring.hxx>
24 #include <rtl/ustrbuf.hxx>
25 #include <sal/log.hxx>
26 #include <osl/diagnose.h>
27 #include <com/sun/star/frame/XModel.hpp>
28 #include <com/sun/star/beans/XPropertySet.hpp>
29 #include <com/sun/star/beans/UnknownPropertyException.hpp>
30 #include <com/sun/star/container/XEnumerationAccess.hpp>
32 #include <com/sun/star/container/XEnumeration.hpp>
33 #include <com/sun/star/document/XRedlinesSupplier.hpp>
34 #include <com/sun/star/text/XText.hpp>
35 #include <com/sun/star/text/XTextContent.hpp>
36 #include <com/sun/star/text/XTextSection.hpp>
37 #include <com/sun/star/util/DateTime.hpp>
39 #include <sax/tools/converter.hxx>
41 #include <xmloff/xmltoken.hxx>
42 #include <xmloff/xmlnamespace.hxx>
43 #include <xmloff/xmlexp.hxx>
44 #include <xmloff/xmluconv.hxx>
45 #include <unotools/securityoptions.hxx>
48 using namespace ::com::sun::star
;
49 using namespace ::xmloff::token
;
51 using ::com::sun::star::beans::PropertyValue
;
52 using ::com::sun::star::beans::XPropertySet
;
53 using ::com::sun::star::beans::UnknownPropertyException
;
54 using ::com::sun::star::document::XRedlinesSupplier
;
55 using ::com::sun::star::container::XEnumerationAccess
;
56 using ::com::sun::star::container::XEnumeration
;
57 using ::com::sun::star::text::XText
;
58 using ::com::sun::star::text::XTextContent
;
59 using ::com::sun::star::text::XTextSection
;
60 using ::com::sun::star::uno::Any
;
61 using ::com::sun::star::uno::Reference
;
62 using ::com::sun::star::uno::Sequence
;
65 XMLRedlineExport::XMLRedlineExport(SvXMLExport
& rExp
)
66 : sDeletion(GetXMLToken(XML_DELETION
))
67 , sFormatChange(GetXMLToken(XML_FORMAT_CHANGE
))
68 , sInsertion(GetXMLToken(XML_INSERTION
))
70 , pCurrentChangesList(nullptr)
75 XMLRedlineExport::~XMLRedlineExport()
80 void XMLRedlineExport::ExportChange(
81 const Reference
<XPropertySet
> & rPropSet
,
86 // For the headers/footers, we have to collect the autostyles
87 // here. For the general case, however, it's better to collect
88 // the autostyles by iterating over the global redline
89 // list. So that's what we do: Here, we collect autostyles
90 // only if we have no current list of changes. For the
91 // main-document case, the autostyles are collected in
92 // ExportChangesListAutoStyles().
93 if (pCurrentChangesList
!= nullptr)
94 ExportChangeAutoStyle(rPropSet
);
98 ExportChangeInline(rPropSet
);
103 void XMLRedlineExport::ExportChangesList(bool bAutoStyles
)
107 ExportChangesListAutoStyles();
111 ExportChangesListElements();
116 void XMLRedlineExport::ExportChangesList(
117 const Reference
<XText
> & rText
,
120 // in the header/footer case, auto styles are collected from the
121 // inline change elements.
125 // look for changes list for this XText
126 ChangesMapType::iterator aFind
= aChangeMap
.find(rText
);
127 if (aFind
== aChangeMap
.end())
130 ChangesVectorType
& rChangesList
= aFind
->second
;
132 // export only if changes are found
133 if (rChangesList
.empty())
136 // changes container element
137 SvXMLElementExport
aChanges(rExport
, XML_NAMESPACE_TEXT
,
141 // iterate over changes list
142 for (auto const& change
: rChangesList
)
144 ExportChangedRegion(change
);
146 // else: changes list empty -> ignore
147 // else: no changes list found -> empty
150 void XMLRedlineExport::SetCurrentXText(
151 const Reference
<XText
> & rText
)
155 // look for appropriate list in map; use the found one, or create new
156 ChangesMapType::iterator aIter
= aChangeMap
.find(rText
);
157 if (aIter
== aChangeMap
.end())
159 auto rv
= aChangeMap
.emplace(std::piecewise_construct
, std::forward_as_tuple(rText
), std::forward_as_tuple());
160 pCurrentChangesList
= &rv
.first
->second
;
163 pCurrentChangesList
= &aIter
->second
;
167 // don't record changes
172 void XMLRedlineExport::SetCurrentXText()
174 pCurrentChangesList
= nullptr;
178 void XMLRedlineExport::ExportChangesListElements()
180 // get redlines (aka tracked changes) from the model
181 Reference
<XRedlinesSupplier
> xSupplier(rExport
.GetModel(), uno::UNO_QUERY
);
185 Reference
<XEnumerationAccess
> aEnumAccess
= xSupplier
->getRedlines();
187 // redline protection key
188 Reference
<XPropertySet
> aDocPropertySet( rExport
.GetModel(),
190 // redlining enabled?
191 bool bEnabled
= *o3tl::doAccess
<bool>(aDocPropertySet
->getPropertyValue(
192 u
"RecordChanges"_ustr
));
194 // only export if we have redlines or attributes
195 if ( !(aEnumAccess
->hasElements() || bEnabled
) )
199 // export only if we have changes, but tracking is not enabled
200 if ( !bEnabled
!= !aEnumAccess
->hasElements() )
202 rExport
.AddAttribute(
203 XML_NAMESPACE_TEXT
, XML_TRACK_CHANGES
,
204 bEnabled
? XML_TRUE
: XML_FALSE
);
207 // changes container element
208 SvXMLElementExport
aChanges(rExport
, XML_NAMESPACE_TEXT
,
212 // get enumeration and iterate over elements
213 Reference
<XEnumeration
> aEnum
= aEnumAccess
->createEnumeration();
214 while (aEnum
->hasMoreElements())
216 Any aAny
= aEnum
->nextElement();
217 Reference
<XPropertySet
> xPropSet
;
220 DBG_ASSERT(xPropSet
.is(),
221 "can't get XPropertySet; skipping Redline");
224 // export only if not in header or footer
225 // (those must be exported with their XText)
226 aAny
= xPropSet
->getPropertyValue(u
"IsInHeaderFooter"_ustr
);
227 if (! *o3tl::doAccess
<bool>(aAny
))
229 // and finally, export change
230 ExportChangedRegion(xPropSet
);
233 // else: no XPropertySet -> no export
235 // else: no redlines -> no export
236 // else: no XRedlineSupplier -> no export
239 void XMLRedlineExport::ExportChangeAutoStyle(
240 const Reference
<XPropertySet
> & rPropSet
)
242 // record change (if changes should be recorded)
243 if (nullptr != pCurrentChangesList
)
245 // put redline in list if it's collapsed or the redline start
246 Any aIsStart
= rPropSet
->getPropertyValue(u
"IsStart"_ustr
);
247 Any aIsCollapsed
= rPropSet
->getPropertyValue(u
"IsCollapsed"_ustr
);
249 if ( *o3tl::doAccess
<bool>(aIsStart
) ||
250 *o3tl::doAccess
<bool>(aIsCollapsed
) )
251 pCurrentChangesList
->push_back(rPropSet
);
254 // get XText for export of redline auto styles
255 Any aAny
= rPropSet
->getPropertyValue(u
"RedlineText"_ustr
);
256 Reference
<XText
> xText
;
260 // export the auto styles
261 rExport
.GetTextParagraphExport()->collectTextAutoStyles(xText
);
265 void XMLRedlineExport::ExportChangesListAutoStyles()
267 // get redlines (aka tracked changes) from the model
268 Reference
<XRedlinesSupplier
> xSupplier(rExport
.GetModel(), uno::UNO_QUERY
);
272 Reference
<XEnumerationAccess
> aEnumAccess
= xSupplier
->getRedlines();
274 // only export if we actually have redlines
275 if (!aEnumAccess
->hasElements())
278 // get enumeration and iterate over elements
279 Reference
<XEnumeration
> aEnum
= aEnumAccess
->createEnumeration();
280 while (aEnum
->hasMoreElements())
282 Any aAny
= aEnum
->nextElement();
283 Reference
<XPropertySet
> xPropSet
;
286 DBG_ASSERT(xPropSet
.is(),
287 "can't get XPropertySet; skipping Redline");
291 // export only if not in header or footer
292 // (those must be exported with their XText)
293 aAny
= xPropSet
->getPropertyValue(u
"IsInHeaderFooter"_ustr
);
294 if (! *o3tl::doAccess
<bool>(aAny
))
296 ExportChangeAutoStyle(xPropSet
);
302 void XMLRedlineExport::ExportChangeInline(
303 const Reference
<XPropertySet
> & rPropSet
)
305 // determine element name (depending on collapsed, start/end)
306 enum XMLTokenEnum eElement
= XML_TOKEN_INVALID
;
307 Any aAny
= rPropSet
->getPropertyValue(u
"IsCollapsed"_ustr
);
308 bool bCollapsed
= *o3tl::doAccess
<bool>(aAny
);
311 eElement
= XML_CHANGE
;
315 aAny
= rPropSet
->getPropertyValue(u
"IsStart"_ustr
);
316 const bool bStart
= *o3tl::doAccess
<bool>(aAny
);
317 eElement
= bStart
? XML_CHANGE_START
: XML_CHANGE_END
;
320 if (XML_TOKEN_INVALID
!= eElement
)
322 // we always need the ID
323 rExport
.AddAttribute(XML_NAMESPACE_TEXT
, XML_CHANGE_ID
,
324 GetRedlineID(rPropSet
));
326 // export the element (no whitespace because we're in the text body)
327 SvXMLElementExport
aChangeElem(rExport
, XML_NAMESPACE_TEXT
,
328 eElement
, false, false);
333 void XMLRedlineExport::ExportChangedRegion(
334 const Reference
<XPropertySet
> & rPropSet
)
337 rExport
.AddAttributeIdLegacy(XML_NAMESPACE_TEXT
, GetRedlineID(rPropSet
));
339 // merge-last-paragraph
340 Any aAny
= rPropSet
->getPropertyValue(u
"MergeLastPara"_ustr
);
341 if( ! *o3tl::doAccess
<bool>(aAny
) )
342 rExport
.AddAttribute(XML_NAMESPACE_TEXT
, XML_MERGE_LAST_PARAGRAPH
,
345 // export change region element
346 SvXMLElementExport
aChangedRegion(rExport
, XML_NAMESPACE_TEXT
,
347 XML_CHANGED_REGION
, true, true);
350 // scope for (first) change element
352 aAny
= rPropSet
->getPropertyValue(u
"RedlineType"_ustr
);
355 SvXMLElementExport
aChange(rExport
, XML_NAMESPACE_TEXT
,
356 ConvertTypeName(sType
), true, true);
358 ExportChangeInfo(rPropSet
);
360 // get XText from the redline and export (if the XText exists)
361 aAny
= rPropSet
->getPropertyValue(u
"RedlineText"_ustr
);
362 Reference
<XText
> xText
;
366 rExport
.GetTextParagraphExport()->exportText(xText
);
367 // default parameters: bProgress, bExportParagraph ???
369 // else: no text interface -> content is inline and will
373 // changed change? Hierarchical changes can only be two levels
374 // deep. Here we check for the second level.
375 aAny
= rPropSet
->getPropertyValue(u
"RedlineSuccessorData"_ustr
);
376 Sequence
<PropertyValue
> aSuccessorData
;
377 aAny
>>= aSuccessorData
;
379 // if we actually got a hierarchical change, make element and
380 // process change info
381 if (aSuccessorData
.hasElements())
383 // The only change that can be "undone" is an insertion -
384 // after all, you can't re-insert a deletion, but you can
385 // delete an insertion. This assumption is asserted in
386 // ExportChangeInfo(Sequence<PropertyValue>&).
387 SvXMLElementExport
aSecondChangeElem(
388 rExport
, XML_NAMESPACE_TEXT
, XML_INSERTION
,
391 ExportChangeInfo(aSuccessorData
);
393 // else: no hierarchical change
397 OUString
const & XMLRedlineExport::ConvertTypeName(
398 std::u16string_view sApiName
)
400 if (sApiName
== u
"Delete")
404 else if (sApiName
== u
"Insert")
408 else if (sApiName
== u
"Format")
410 return sFormatChange
;
414 OSL_FAIL("unknown redline type");
415 static constexpr OUString
sUnknownChange(u
"UnknownChange"_ustr
);
416 return sUnknownChange
;
421 /** Create a Redline-ID */
422 OUString
XMLRedlineExport::GetRedlineID(
423 const Reference
<XPropertySet
> & rPropSet
)
425 Any aAny
= rPropSet
->getPropertyValue(u
"RedlineIdentifier"_ustr
);
433 void XMLRedlineExport::ExportChangeInfo(
434 const Reference
<XPropertySet
> & rPropSet
)
436 bool bRemovePersonalInfo
= SvtSecurityOptions::IsOptionSet(
437 SvtSecurityOptions::EOption::DocWarnRemovePersonalInfo
) && !SvtSecurityOptions::IsOptionSet(
438 SvtSecurityOptions::EOption::DocWarnKeepRedlineInfo
);
440 SvXMLElementExport
aChangeInfo(rExport
, XML_NAMESPACE_OFFICE
,
441 XML_CHANGE_INFO
, true, true);
443 Any aAny
= rPropSet
->getPropertyValue(u
"RedlineAuthor"_ustr
);
448 SvXMLElementExport
aCreatorElem( rExport
, XML_NAMESPACE_DC
,
451 rExport
.Characters(bRemovePersonalInfo
452 ? "Author" + OUString::number(rExport
.GetInfoID(sTmp
))
456 aAny
= rPropSet
->getPropertyValue(u
"RedlineMovedID"_ustr
);
461 SvXMLElementExport
aCreatorElem(rExport
, XML_NAMESPACE_LO_EXT
, XML_MOVE_ID
, true, false);
462 rExport
.Characters( OUString::number( nTmp
) );
465 aAny
= rPropSet
->getPropertyValue(u
"RedlineDateTime"_ustr
);
466 util::DateTime aDateTime
;
470 ::sax::Converter::convertDateTime(sBuf
, bRemovePersonalInfo
471 ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
472 : aDateTime
, nullptr, true);
473 SvXMLElementExport
aDateElem( rExport
, XML_NAMESPACE_DC
,
476 rExport
.Characters(sBuf
.makeStringAndClear());
479 // comment as <text:p> sequence
480 aAny
= rPropSet
->getPropertyValue(u
"RedlineComment"_ustr
);
482 WriteComment( sTmp
);
485 // write RedlineSuccessorData
486 void XMLRedlineExport::ExportChangeInfo(
487 const Sequence
<PropertyValue
> & rPropertyValues
)
490 bool bRemovePersonalInfo
= SvtSecurityOptions::IsOptionSet(
491 SvtSecurityOptions::EOption::DocWarnRemovePersonalInfo
) && !SvtSecurityOptions::IsOptionSet(
492 SvtSecurityOptions::EOption::DocWarnKeepRedlineInfo
);
494 SvXMLElementExport
aChangeInfo(rExport
, XML_NAMESPACE_OFFICE
,
495 XML_CHANGE_INFO
, true, true);
497 for(const PropertyValue
& rVal
: rPropertyValues
)
499 if( rVal
.Name
== "RedlineAuthor" )
505 SvXMLElementExport
aCreatorElem( rExport
, XML_NAMESPACE_DC
,
508 rExport
.Characters(bRemovePersonalInfo
509 ? "Author" + OUString::number(rExport
.GetInfoID(sTmp
))
513 else if( rVal
.Name
== "RedlineComment" )
515 rVal
.Value
>>= sComment
;
517 else if( rVal
.Name
== "RedlineDateTime" )
519 util::DateTime aDateTime
;
520 rVal
.Value
>>= aDateTime
;
522 ::sax::Converter::convertDateTime(sBuf
, bRemovePersonalInfo
523 ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time
524 : aDateTime
, nullptr, true);
525 SvXMLElementExport
aDateElem( rExport
, XML_NAMESPACE_DC
,
528 rExport
.Characters(sBuf
.makeStringAndClear());
530 else if( rVal
.Name
== "RedlineType" )
532 // check if this is an insertion; cf. comment at calling location
535 DBG_ASSERT(sTmp
== "Insert",
536 "hierarchical change must be insertion");
538 // else: unknown value -> ignore
541 // finally write comment paragraphs
542 WriteComment( sComment
);
545 void XMLRedlineExport::ExportStartOrEndRedline(
546 const Reference
<XPropertySet
> & rPropSet
,
549 if( ! rPropSet
.is() )
552 // get appropriate (start or end) property
556 aAny
= rPropSet
->getPropertyValue(bStart
? u
"StartRedline"_ustr
: u
"EndRedline"_ustr
);
558 catch(const UnknownPropertyException
&)
560 // If we don't have the property, there's nothing to do.
564 Sequence
<PropertyValue
> aValues
;
567 // seek for redline properties
568 bool bIsCollapsed
= false;
569 bool bIsStart
= true;
571 bool bIdOK
= false; // have we seen an ID?
572 for (const auto& rValue
: aValues
)
574 if (rValue
.Name
== "RedlineIdentifier")
576 rValue
.Value
>>= sId
;
579 else if (rValue
.Name
== "IsCollapsed")
581 bIsCollapsed
= *o3tl::doAccess
<bool>(rValue
.Value
);
583 else if (rValue
.Name
== "IsStart")
585 bIsStart
= *o3tl::doAccess
<bool>(rValue
.Value
);
592 SAL_WARN_IF( sId
.isEmpty(), "xmloff", "Redlines must have IDs" );
594 // TODO: use GetRedlineID or eliminate that function
595 rExport
.AddAttribute(XML_NAMESPACE_TEXT
, XML_CHANGE_ID
,
598 // export the element
599 // (whitespace because we're not inside paragraphs)
600 SvXMLElementExport
aChangeElem(
601 rExport
, XML_NAMESPACE_TEXT
,
602 bIsCollapsed
? XML_CHANGE
:
603 ( bIsStart
? XML_CHANGE_START
: XML_CHANGE_END
),
607 void XMLRedlineExport::ExportStartOrEndRedline(
608 const Reference
<XTextContent
> & rContent
,
611 Reference
<XPropertySet
> xPropSet(rContent
, uno::UNO_QUERY
);
614 ExportStartOrEndRedline(xPropSet
, bStart
);
618 OSL_FAIL("XPropertySet expected");
622 void XMLRedlineExport::ExportStartOrEndRedline(
623 const Reference
<XTextSection
> & rSection
,
626 Reference
<XPropertySet
> xPropSet(rSection
, uno::UNO_QUERY
);
629 ExportStartOrEndRedline(xPropSet
, bStart
);
633 OSL_FAIL("XPropertySet expected");
637 void XMLRedlineExport::WriteComment(std::u16string_view rComment
)
639 if (rComment
.empty())
642 // iterate over all string-pieces separated by return (0x0a) and
643 // put each inside a paragraph element.
644 SvXMLTokenEnumerator
aEnumerator(rComment
, char(0x0a));
645 std::u16string_view aSubString
;
646 while (aEnumerator
.getNextToken(aSubString
))
648 SvXMLElementExport
aParagraph(
649 rExport
, XML_NAMESPACE_TEXT
, XML_P
, true, false);
650 rExport
.Characters(OUString(aSubString
));
654 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */