/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include "XMLRedlineExport.hxx" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ::com::sun::star; using namespace ::xmloff::token; using ::com::sun::star::beans::PropertyValue; using ::com::sun::star::beans::XPropertySet; using ::com::sun::star::beans::UnknownPropertyException; using ::com::sun::star::document::XRedlinesSupplier; using ::com::sun::star::container::XEnumerationAccess; using ::com::sun::star::container::XEnumeration; using ::com::sun::star::text::XText; using ::com::sun::star::text::XTextContent; using ::com::sun::star::text::XTextSection; using ::com::sun::star::uno::Any; using ::com::sun::star::uno::Reference; using ::com::sun::star::uno::Sequence; XMLRedlineExport::XMLRedlineExport(SvXMLExport& rExp) : sDeletion(GetXMLToken(XML_DELETION)) , sFormatChange(GetXMLToken(XML_FORMAT_CHANGE)) , sInsertion(GetXMLToken(XML_INSERTION)) , rExport(rExp) , pCurrentChangesList(nullptr) { } XMLRedlineExport::~XMLRedlineExport() { } void XMLRedlineExport::ExportChange( const Reference & rPropSet, bool bAutoStyle) { if (bAutoStyle) { // For the headers/footers, we have to collect the autostyles // here. For the general case, however, it's better to collect // the autostyles by iterating over the global redline // list. So that's what we do: Here, we collect autostyles // only if we have no current list of changes. For the // main-document case, the autostyles are collected in // ExportChangesListAutoStyles(). if (pCurrentChangesList != nullptr) ExportChangeAutoStyle(rPropSet); } else { ExportChangeInline(rPropSet); } } void XMLRedlineExport::ExportChangesList(bool bAutoStyles) { if (bAutoStyles) { ExportChangesListAutoStyles(); } else { ExportChangesListElements(); } } void XMLRedlineExport::ExportChangesList( const Reference & rText, bool bAutoStyles) { // in the header/footer case, auto styles are collected from the // inline change elements. if (bAutoStyles) return; // look for changes list for this XText ChangesMapType::iterator aFind = aChangeMap.find(rText); if (aFind == aChangeMap.end()) return; ChangesVectorType& rChangesList = aFind->second; // export only if changes are found if (rChangesList.empty()) return; // changes container element SvXMLElementExport aChanges(rExport, XML_NAMESPACE_TEXT, XML_TRACKED_CHANGES, true, true); // iterate over changes list for (auto const& change : rChangesList) { ExportChangedRegion(change); } // else: changes list empty -> ignore // else: no changes list found -> empty } void XMLRedlineExport::SetCurrentXText( const Reference & rText) { if (rText.is()) { // look for appropriate list in map; use the found one, or create new ChangesMapType::iterator aIter = aChangeMap.find(rText); if (aIter == aChangeMap.end()) { auto rv = aChangeMap.emplace(std::piecewise_construct, std::forward_as_tuple(rText), std::forward_as_tuple()); pCurrentChangesList = &rv.first->second; } else pCurrentChangesList = &aIter->second; } else { // don't record changes SetCurrentXText(); } } void XMLRedlineExport::SetCurrentXText() { pCurrentChangesList = nullptr; } void XMLRedlineExport::ExportChangesListElements() { // get redlines (aka tracked changes) from the model Reference xSupplier(rExport.GetModel(), uno::UNO_QUERY); if (!xSupplier.is()) return; Reference aEnumAccess = xSupplier->getRedlines(); // redline protection key Reference aDocPropertySet( rExport.GetModel(), uno::UNO_QUERY ); // redlining enabled? bool bEnabled = *o3tl::doAccess(aDocPropertySet->getPropertyValue( "RecordChanges" )); // only export if we have redlines or attributes if ( !(aEnumAccess->hasElements() || bEnabled) ) return; // export only if we have changes, but tracking is not enabled if ( !bEnabled != !aEnumAccess->hasElements() ) { rExport.AddAttribute( XML_NAMESPACE_TEXT, XML_TRACK_CHANGES, bEnabled ? XML_TRUE : XML_FALSE ); } // changes container element SvXMLElementExport aChanges(rExport, XML_NAMESPACE_TEXT, XML_TRACKED_CHANGES, true, true); // get enumeration and iterate over elements Reference aEnum = aEnumAccess->createEnumeration(); while (aEnum->hasMoreElements()) { Any aAny = aEnum->nextElement(); Reference xPropSet; aAny >>= xPropSet; DBG_ASSERT(xPropSet.is(), "can't get XPropertySet; skipping Redline"); if (xPropSet.is()) { // export only if not in header or footer // (those must be exported with their XText) aAny = xPropSet->getPropertyValue("IsInHeaderFooter"); if (! *o3tl::doAccess(aAny)) { // and finally, export change ExportChangedRegion(xPropSet); } } // else: no XPropertySet -> no export } // else: no redlines -> no export // else: no XRedlineSupplier -> no export } void XMLRedlineExport::ExportChangeAutoStyle( const Reference & rPropSet) { // record change (if changes should be recorded) if (nullptr != pCurrentChangesList) { // put redline in list if it's collapsed or the redline start Any aIsStart = rPropSet->getPropertyValue("IsStart"); Any aIsCollapsed = rPropSet->getPropertyValue("IsCollapsed"); if ( *o3tl::doAccess(aIsStart) || *o3tl::doAccess(aIsCollapsed) ) pCurrentChangesList->push_back(rPropSet); } // get XText for export of redline auto styles Any aAny = rPropSet->getPropertyValue("RedlineText"); Reference xText; aAny >>= xText; if (xText.is()) { // export the auto styles rExport.GetTextParagraphExport()->collectTextAutoStyles(xText); } } void XMLRedlineExport::ExportChangesListAutoStyles() { // get redlines (aka tracked changes) from the model Reference xSupplier(rExport.GetModel(), uno::UNO_QUERY); if (!xSupplier.is()) return; Reference aEnumAccess = xSupplier->getRedlines(); // only export if we actually have redlines if (!aEnumAccess->hasElements()) return; // get enumeration and iterate over elements Reference aEnum = aEnumAccess->createEnumeration(); while (aEnum->hasMoreElements()) { Any aAny = aEnum->nextElement(); Reference xPropSet; aAny >>= xPropSet; DBG_ASSERT(xPropSet.is(), "can't get XPropertySet; skipping Redline"); if (xPropSet.is()) { // export only if not in header or footer // (those must be exported with their XText) aAny = xPropSet->getPropertyValue("IsInHeaderFooter"); if (! *o3tl::doAccess(aAny)) { ExportChangeAutoStyle(xPropSet); } } } } void XMLRedlineExport::ExportChangeInline( const Reference & rPropSet) { // determine element name (depending on collapsed, start/end) enum XMLTokenEnum eElement = XML_TOKEN_INVALID; Any aAny = rPropSet->getPropertyValue("IsCollapsed"); bool bCollapsed = *o3tl::doAccess(aAny); if (bCollapsed) { eElement = XML_CHANGE; } else { aAny = rPropSet->getPropertyValue("IsStart"); const bool bStart = *o3tl::doAccess(aAny); eElement = bStart ? XML_CHANGE_START : XML_CHANGE_END; } if (XML_TOKEN_INVALID != eElement) { // we always need the ID rExport.AddAttribute(XML_NAMESPACE_TEXT, XML_CHANGE_ID, GetRedlineID(rPropSet)); // export the element (no whitespace because we're in the text body) SvXMLElementExport aChangeElem(rExport, XML_NAMESPACE_TEXT, eElement, false, false); } } void XMLRedlineExport::ExportChangedRegion( const Reference & rPropSet) { // Redline-ID rExport.AddAttributeIdLegacy(XML_NAMESPACE_TEXT, GetRedlineID(rPropSet)); // merge-last-paragraph Any aAny = rPropSet->getPropertyValue("MergeLastPara"); if( ! *o3tl::doAccess(aAny) ) rExport.AddAttribute(XML_NAMESPACE_TEXT, XML_MERGE_LAST_PARAGRAPH, XML_FALSE); // export change region element SvXMLElementExport aChangedRegion(rExport, XML_NAMESPACE_TEXT, XML_CHANGED_REGION, true, true); // scope for (first) change element { aAny = rPropSet->getPropertyValue("RedlineType"); OUString sType; aAny >>= sType; SvXMLElementExport aChange(rExport, XML_NAMESPACE_TEXT, ConvertTypeName(sType), true, true); ExportChangeInfo(rPropSet); // get XText from the redline and export (if the XText exists) aAny = rPropSet->getPropertyValue("RedlineText"); Reference xText; aAny >>= xText; if (xText.is()) { rExport.GetTextParagraphExport()->exportText(xText); // default parameters: bProgress, bExportParagraph ??? } // else: no text interface -> content is inline and will // be exported there } // changed change? Hierarchical changes can only be two levels // deep. Here we check for the second level. aAny = rPropSet->getPropertyValue("RedlineSuccessorData"); Sequence aSuccessorData; aAny >>= aSuccessorData; // if we actually got a hierarchical change, make element and // process change info if (aSuccessorData.hasElements()) { // The only change that can be "undone" is an insertion - // after all, you can't re-insert a deletion, but you can // delete an insertion. This assumption is asserted in // ExportChangeInfo(Sequence&). SvXMLElementExport aSecondChangeElem( rExport, XML_NAMESPACE_TEXT, XML_INSERTION, true, true); ExportChangeInfo(aSuccessorData); } // else: no hierarchical change } OUString const & XMLRedlineExport::ConvertTypeName( std::u16string_view sApiName) { if (sApiName == u"Delete") { return sDeletion; } else if (sApiName == u"Insert") { return sInsertion; } else if (sApiName == u"Format") { return sFormatChange; } else { OSL_FAIL("unknown redline type"); static const OUString sUnknownChange("UnknownChange"); return sUnknownChange; } } /** Create a Redline-ID */ OUString XMLRedlineExport::GetRedlineID( const Reference & rPropSet) { Any aAny = rPropSet->getPropertyValue("RedlineIdentifier"); OUString sTmp; aAny >>= sTmp; return "ct" + sTmp; } void XMLRedlineExport::ExportChangeInfo( const Reference & rPropSet) { bool bRemovePersonalInfo = SvtSecurityOptions::IsOptionSet( SvtSecurityOptions::EOption::DocWarnRemovePersonalInfo ); SvXMLElementExport aChangeInfo(rExport, XML_NAMESPACE_OFFICE, XML_CHANGE_INFO, true, true); Any aAny = rPropSet->getPropertyValue("RedlineAuthor"); OUString sTmp; aAny >>= sTmp; if (!sTmp.isEmpty()) { SvXMLElementExport aCreatorElem( rExport, XML_NAMESPACE_DC, XML_CREATOR, true, false ); rExport.Characters(bRemovePersonalInfo ? "Author" + OUString::number(rExport.GetInfoID(sTmp)) : sTmp ); } aAny = rPropSet->getPropertyValue("RedlineDateTime"); util::DateTime aDateTime; aAny >>= aDateTime; { OUStringBuffer sBuf; ::sax::Converter::convertDateTime(sBuf, bRemovePersonalInfo ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time : aDateTime, nullptr); SvXMLElementExport aDateElem( rExport, XML_NAMESPACE_DC, XML_DATE, true, false ); rExport.Characters(sBuf.makeStringAndClear()); } // comment as sequence aAny = rPropSet->getPropertyValue("RedlineComment"); aAny >>= sTmp; WriteComment( sTmp ); } // write RedlineSuccessorData void XMLRedlineExport::ExportChangeInfo( const Sequence & rPropertyValues) { OUString sComment; bool bRemovePersonalInfo = SvtSecurityOptions::IsOptionSet( SvtSecurityOptions::EOption::DocWarnRemovePersonalInfo ); SvXMLElementExport aChangeInfo(rExport, XML_NAMESPACE_OFFICE, XML_CHANGE_INFO, true, true); for(const PropertyValue& rVal : rPropertyValues) { if( rVal.Name == "RedlineAuthor" ) { OUString sTmp; rVal.Value >>= sTmp; if (!sTmp.isEmpty()) { SvXMLElementExport aCreatorElem( rExport, XML_NAMESPACE_DC, XML_CREATOR, true, false ); rExport.Characters(bRemovePersonalInfo ? "Author" + OUString::number(rExport.GetInfoID(sTmp)) : sTmp ); } } else if( rVal.Name == "RedlineComment" ) { rVal.Value >>= sComment; } else if( rVal.Name == "RedlineDateTime" ) { util::DateTime aDateTime; rVal.Value >>= aDateTime; OUStringBuffer sBuf; ::sax::Converter::convertDateTime(sBuf, bRemovePersonalInfo ? util::DateTime(0, 0, 0, 0, 1, 1, 1970, true) // Epoch time : aDateTime, nullptr); SvXMLElementExport aDateElem( rExport, XML_NAMESPACE_DC, XML_DATE, true, false ); rExport.Characters(sBuf.makeStringAndClear()); } else if( rVal.Name == "RedlineType" ) { // check if this is an insertion; cf. comment at calling location OUString sTmp; rVal.Value >>= sTmp; DBG_ASSERT(sTmp == "Insert", "hierarchical change must be insertion"); } // else: unknown value -> ignore } // finally write comment paragraphs WriteComment( sComment ); } void XMLRedlineExport::ExportStartOrEndRedline( const Reference & rPropSet, bool bStart) { if( ! rPropSet.is() ) return; // get appropriate (start or end) property Any aAny; try { aAny = rPropSet->getPropertyValue(bStart ? OUString("StartRedline") : OUString("EndRedline")); } catch(const UnknownPropertyException&) { // If we don't have the property, there's nothing to do. return; } Sequence aValues; aAny >>= aValues; // seek for redline properties bool bIsCollapsed = false; bool bIsStart = true; OUString sId; bool bIdOK = false; // have we seen an ID? for(const auto& rValue : std::as_const(aValues)) { if (rValue.Name == "RedlineIdentifier") { rValue.Value >>= sId; bIdOK = true; } else if (rValue.Name == "IsCollapsed") { bIsCollapsed = *o3tl::doAccess(rValue.Value); } else if (rValue.Name == "IsStart") { bIsStart = *o3tl::doAccess(rValue.Value); } } if( !bIdOK ) return; SAL_WARN_IF( sId.isEmpty(), "xmloff", "Redlines must have IDs" ); // TODO: use GetRedlineID or eliminate that function rExport.AddAttribute(XML_NAMESPACE_TEXT, XML_CHANGE_ID, "ct" + sId); // export the element // (whitespace because we're not inside paragraphs) SvXMLElementExport aChangeElem( rExport, XML_NAMESPACE_TEXT, bIsCollapsed ? XML_CHANGE : ( bIsStart ? XML_CHANGE_START : XML_CHANGE_END ), true, true); } void XMLRedlineExport::ExportStartOrEndRedline( const Reference & rContent, bool bStart) { Reference xPropSet(rContent, uno::UNO_QUERY); if (xPropSet.is()) { ExportStartOrEndRedline(xPropSet, bStart); } else { OSL_FAIL("XPropertySet expected"); } } void XMLRedlineExport::ExportStartOrEndRedline( const Reference & rSection, bool bStart) { Reference xPropSet(rSection, uno::UNO_QUERY); if (xPropSet.is()) { ExportStartOrEndRedline(xPropSet, bStart); } else { OSL_FAIL("XPropertySet expected"); } } void XMLRedlineExport::WriteComment(std::u16string_view rComment) { if (rComment.empty()) return; // iterate over all string-pieces separated by return (0x0a) and // put each inside a paragraph element. SvXMLTokenEnumerator aEnumerator(rComment, char(0x0a)); std::u16string_view aSubString; while (aEnumerator.getNextToken(aSubString)) { SvXMLElementExport aParagraph( rExport, XML_NAMESPACE_TEXT, XML_P, true, false); rExport.Characters(OUString(aSubString)); } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */