/* -*- 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 "XMLTextMarkImportContext.hxx" #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 ::com::sun::star::text; using namespace ::com::sun::star::uno; using namespace ::com::sun::star::beans; using namespace ::com::sun::star::lang; using namespace ::com::sun::star::container; using namespace ::com::sun::star::xml::sax; using namespace ::xmloff::token; XMLFieldParamImportContext::XMLFieldParamImportContext( SvXMLImport& rImport, XMLTextImportHelper& rHlp ) : SvXMLImportContext(rImport), rHelper(rHlp) { } void XMLFieldParamImportContext::startFastElement(sal_Int32 /*nElement*/, const css::uno::Reference< css::xml::sax::XFastAttributeList> & xAttrList) { OUString sName; OUString sValue; for (auto &aIter : sax_fastparser::castToFastAttributeList( xAttrList )) { switch (aIter.getToken()) { case XML_ELEMENT(FIELD, XML_NAME): sName = aIter.toString(); break; case XML_ELEMENT(FIELD, XML_VALUE): sValue = aIter.toString(); break; default: XMLOFF_WARN_UNKNOWN("xmloff", aIter); } } if (rHelper.hasCurrentFieldCtx() && !sName.isEmpty()) { rHelper.addFieldParam(sName, sValue); } } XMLTextMarkImportContext::XMLTextMarkImportContext( SvXMLImport& rImport, XMLTextImportHelper& rHlp, uno::Reference & io_rxCrossRefHeadingBookmark ) : SvXMLImportContext(rImport) , m_rHelper(rHlp) , m_rxCrossRefHeadingBookmark(io_rxCrossRefHeadingBookmark) , m_isHidden(false) , m_bHaveAbout(false) { } namespace { enum lcl_MarkType { TypeReference, TypeReferenceStart, TypeReferenceEnd, TypeBookmark, TypeBookmarkStart, TypeBookmarkEnd, TypeFieldmark, TypeFieldmarkStart, TypeFieldmarkSeparator, TypeFieldmarkEnd }; } SvXMLEnumMapEntry const lcl_aMarkTypeMap[] = { { XML_REFERENCE_MARK, TypeReference }, { XML_REFERENCE_MARK_START, TypeReferenceStart }, { XML_REFERENCE_MARK_END, TypeReferenceEnd }, { XML_BOOKMARK, TypeBookmark }, { XML_BOOKMARK_START, TypeBookmarkStart }, { XML_BOOKMARK_END, TypeBookmarkEnd }, { XML_FIELDMARK, TypeFieldmark }, { XML_FIELDMARK_START, TypeFieldmarkStart }, { XML_FIELDMARK_SEPARATOR, TypeFieldmarkSeparator }, { XML_FIELDMARK_END, TypeFieldmarkEnd }, { XML_TOKEN_INVALID, lcl_MarkType(0) }, }; static OUString lcl_getFormFieldmarkName(std::u16string_view name) { if (name == ODF_FORMCHECKBOX || name == u"msoffice.field.FORMCHECKBOX" || name == u"ecma.office-open-xml.field.FORMCHECKBOX") return ODF_FORMCHECKBOX; else if (name == ODF_FORMDROPDOWN || name == u"ecma.office-open-xml.field.FORMDROPDOWN") return ODF_FORMDROPDOWN; else return OUString(); } static OUString lcl_getFieldmarkName(OUString const& name) { if (name == "msoffice.field.FORMTEXT" || name == "ecma.office-open-xml.field.FORMTEXT") return ODF_FORMTEXT; else return name; } void XMLTextMarkImportContext::startFastElement( sal_Int32 nElement, const css::uno::Reference< css::xml::sax::XFastAttributeList >& xAttrList ) { if (!FindName(xAttrList)) { m_sBookmarkName.clear(); } if ((nElement & TOKEN_MASK) == XML_FIELDMARK_START || (nElement & TOKEN_MASK) == XML_FIELDMARK) { if (m_sBookmarkName.isEmpty()) { m_sBookmarkName = "Unknown"; } m_rHelper.pushFieldCtx( m_sBookmarkName, m_sFieldName ); } if ((nElement & TOKEN_MASK) == XML_BOOKMARK_START) { m_rHelper.setBookmarkAttributes(m_sBookmarkName, m_isHidden, m_sCondition); } } static auto InsertFieldmark(SvXMLImport & rImport, XMLTextImportHelper & rHelper, bool const isFieldmarkSeparatorMissing) -> void { assert(rHelper.hasCurrentFieldCtx()); // was set up in StartElement() // fdo#86795 check if it's actually a checkbox first auto const [ name, type ] = rHelper.getCurrentFieldType(); OUString const fieldmarkTypeName = lcl_getFieldmarkName(type); if (fieldmarkTypeName == ODF_FORMCHECKBOX || fieldmarkTypeName == ODF_FORMDROPDOWN) { // sw can't handle checkbox with start+end SAL_INFO("xmloff.text", "invalid fieldmark-start/fieldmark-end ignored"); return; } uno::Reference const xStartRange(rHelper.getCurrentFieldStart()); uno::Reference const xCursor( rHelper.GetText()->createTextCursorByRange(xStartRange)); uno::Reference const xCompare(rHelper.GetText(), uno::UNO_QUERY); if (xCompare->compareRegionStarts(xStartRange, rHelper.GetCursorAsRange()) < 0) { SAL_WARN("xmloff.text", "invalid field mark positions"); assert(false); } xCursor->gotoRange(rHelper.GetCursorAsRange(), true); Reference const xContent = XMLTextMarkImportContext::CreateAndInsertMark( rImport, "com.sun.star.text.Fieldmark", name, xCursor, OUString(), isFieldmarkSeparatorMissing); if (!xContent.is()) return; // setup fieldmark... Reference const xFormField(xContent, UNO_QUERY); assert(xFormField.is()); try { xFormField->setFieldType(fieldmarkTypeName); } catch (uno::RuntimeException const&) { // tdf#140437 somehow old documents had the field code in the type // attribute instead of field:param SAL_INFO("xmloff.text", "invalid fieldmark type, converting to param"); // add without checking: FieldParamImporter::Import() catches ElementExistException rHelper.addFieldParam(ODF_CODE_PARAM, fieldmarkTypeName); xFormField->setFieldType(ODF_UNHANDLED); } rHelper.setCurrentFieldParamsTo(xFormField); // move cursor after setFieldType as that may delete/re-insert rHelper.GetCursor()->gotoRange(xContent->getAnchor()->getEnd(), false); rHelper.GetCursor()->goLeft(1, false); // move before CH_TXT_ATR_FIELDEND // tdf#129520: AppendTextNode() ignores the content index! // plan B: insert a spurious paragraph break now and join // it in PopFieldmark()! rHelper.GetText()->insertControlCharacter(rHelper.GetCursor(), text::ControlCharacter::PARAGRAPH_BREAK, false); rHelper.GetCursor()->goLeft(1, false); // back to previous paragraph } static auto PopFieldmark(XMLTextImportHelper & rHelper) -> void { // can't verify name because it's not written as an attribute... uno::Reference const xField(rHelper.popFieldCtx(), uno::UNO_QUERY); if (!xField.is()) return; if (rHelper.GetText() == xField->getAnchor()->getText()) { try { // skip CH_TXT_ATR_FIELDEND rHelper.GetCursor()->goRight(1, true); rHelper.GetCursor()->setString(OUString()); // undo AppendTextNode from InsertFieldmark rHelper.GetCursor()->gotoRange(xField->getAnchor()->getEnd(), false); } catch (uno::Exception const&) { assert(false); // must succeed } } else { SAL_INFO("xmloff.text", "fieldmark has invalid positions"); // could either dispose it or leave it to end at the end of the document? xField->dispose(); } } void XMLTextMarkImportContext::endFastElement(sal_Int32 nElement) { static constexpr OUStringLiteral sAPI_bookmark = u"com.sun.star.text.Bookmark"; lcl_MarkType nTmp{}; if (!SvXMLUnitConverter::convertEnum(nTmp, SvXMLImport::getNameFromToken(nElement), lcl_aMarkTypeMap)) return; if (m_sBookmarkName.isEmpty() && TypeFieldmarkEnd != nTmp && TypeFieldmarkSeparator != nTmp) return; switch (nTmp) { case TypeReference: // export point reference mark CreateAndInsertMark(GetImport(), "com.sun.star.text.ReferenceMark", m_sBookmarkName, m_rHelper.GetCursorAsRange()->getStart()); break; case TypeBookmark: { // tdf#94804: detect duplicate heading cross reference bookmarks if (m_sBookmarkName.startsWith("__RefHeading__")) { if (m_rxCrossRefHeadingBookmark.is()) { uno::Reference const xNamed( m_rxCrossRefHeadingBookmark, uno::UNO_QUERY); m_rHelper.AddCrossRefHeadingMapping( m_sBookmarkName, xNamed->getName()); break; // don't insert } } } [[fallthrough]]; case TypeFieldmark: { const OUString formFieldmarkName=lcl_getFormFieldmarkName(m_sFieldName); bool bImportAsField = (nTmp==TypeFieldmark && !formFieldmarkName.isEmpty()); //@TODO handle abbreviation cases... // export point bookmark const Reference xContent( CreateAndInsertMark(GetImport(), (bImportAsField ? OUString("com.sun.star.text.FormFieldmark") : OUString(sAPI_bookmark)), m_sBookmarkName, m_rHelper.GetCursorAsRange()->getStart(), m_sXmlId) ); if (nTmp==TypeFieldmark) { if (xContent.is() && bImportAsField) { // setup fieldmark... Reference< css::text::XFormField> xFormField(xContent, UNO_QUERY); xFormField->setFieldType(formFieldmarkName); if (xFormField.is() && m_rHelper.hasCurrentFieldCtx()) { m_rHelper.setCurrentFieldParamsTo(xFormField); } } m_rHelper.popFieldCtx(); } if (TypeBookmark == nTmp && m_sBookmarkName.startsWith("__RefHeading__")) { assert(xContent.is()); m_rxCrossRefHeadingBookmark = xContent; } } break; case TypeBookmarkStart: // save XTextRange for later construction of bookmark { std::shared_ptr< ::xmloff::ParsedRDFaAttributes > xRDFaAttributes; if (m_bHaveAbout && TypeBookmarkStart == nTmp) { xRDFaAttributes = GetImport().GetRDFaImportHelper().ParseRDFa( m_sAbout, m_sProperty, m_sContent, m_sDatatype); } m_rHelper.InsertBookmarkStartRange( m_sBookmarkName, m_rHelper.GetCursorAsRange()->getStart(), m_sXmlId, xRDFaAttributes); } break; case TypeBookmarkEnd: { // tdf#94804: detect duplicate heading cross reference bookmarks if (m_sBookmarkName.startsWith("__RefHeading__")) { if (m_rxCrossRefHeadingBookmark.is()) { uno::Reference const xNamed( m_rxCrossRefHeadingBookmark, uno::UNO_QUERY); m_rHelper.AddCrossRefHeadingMapping( m_sBookmarkName, xNamed->getName()); break; // don't insert } } // get old range, and construct Reference xStartRange; std::shared_ptr< ::xmloff::ParsedRDFaAttributes > xRDFaAttributes; if (m_rHelper.FindAndRemoveBookmarkStartRange( m_sBookmarkName, xStartRange, m_sXmlId, xRDFaAttributes)) { Reference xEndRange( m_rHelper.GetCursorAsRange()->getStart()); // check if beginning and end are in same XText if (xStartRange.is() && xEndRange.is() && xStartRange->getText() == xEndRange->getText()) { // create range for insertion Reference xInsertionCursor = m_rHelper.GetText()->createTextCursorByRange( xEndRange); try { xInsertionCursor->gotoRange(xStartRange, true); } catch (uno::Exception&) { OSL_ENSURE(false, "cannot go to end position of bookmark"); } //DBG_ASSERT(! xInsertionCursor->isCollapsed(), // "we want no point mark"); // can't assert, because someone could // create a file with subsequence // start/end elements Reference xContent; // insert reference xContent = CreateAndInsertMark(GetImport(), sAPI_bookmark, m_sBookmarkName, xInsertionCursor, m_sXmlId); if (xRDFaAttributes) { const Reference xMeta(xContent, UNO_QUERY); GetImport().GetRDFaImportHelper().AddRDFa( xMeta, xRDFaAttributes); } const Reference xPropertySet(xContent, UNO_QUERY); if (xPropertySet.is()) { xPropertySet->setPropertyValue("BookmarkHidden", uno::Any(m_rHelper.getBookmarkHidden(m_sBookmarkName))); xPropertySet->setPropertyValue("BookmarkCondition", uno::Any(m_rHelper.getBookmarkCondition(m_sBookmarkName))); } if (m_sBookmarkName.startsWith("__RefHeading__")) { assert(xContent.is()); m_rxCrossRefHeadingBookmark = xContent; } } // else: beginning/end in different XText -> ignore! } // else: no start found -> ignore! break; } case TypeFieldmarkStart: { break; } case TypeFieldmarkSeparator: { InsertFieldmark(GetImport(), m_rHelper, false); break; } case TypeFieldmarkEnd: { if (m_rHelper.hasCurrentFieldCtx() && !m_rHelper.hasCurrentFieldSeparator()) { // backward compat for old files without separator InsertFieldmark(GetImport(), m_rHelper, true); } PopFieldmark(m_rHelper); break; } case TypeReferenceStart: case TypeReferenceEnd: OSL_FAIL("reference start/end are handled in txtparai !"); break; default: OSL_FAIL("unknown mark type"); break; } } css::uno::Reference< css::xml::sax::XFastContextHandler > XMLTextMarkImportContext::createFastChildContext( sal_Int32 , const css::uno::Reference< css::xml::sax::XFastAttributeList >& ) { return new XMLFieldParamImportContext(GetImport(), m_rHelper); } Reference XMLTextMarkImportContext::CreateAndInsertMark( SvXMLImport& rImport, const OUString& sServiceName, const OUString& sMarkName, const Reference & rRange, const OUString& i_rXmlId, bool const isFieldmarkSeparatorMissing) { // create mark const Reference xFactory(rImport.GetModel(), UNO_QUERY); Reference xIfc; if (xFactory.is()) { xIfc = xFactory->createInstance(sServiceName); if (!xIfc.is()) { OSL_FAIL("CreateAndInsertMark: cannot create service?"); return nullptr; } // set name (unless there is no name (text:meta)) const Reference xNamed(xIfc, UNO_QUERY); if (xNamed.is()) { xNamed->setName(sMarkName); } else { if (!sMarkName.isEmpty()) { OSL_FAIL("name given, but XNamed not supported?"); return nullptr; } } if (isFieldmarkSeparatorMissing) { uno::Reference const xProps(xIfc, uno::UNO_QUERY_THROW); xProps->setPropertyValue("PrivateSeparatorAtStart", uno::Any(true)); } // cast to XTextContent and attach to document const Reference xTextContent(xIfc, UNO_QUERY); if (xTextContent.is()) { try { // if inserting marks, bAbsorb==sal_False will cause // collapsing of the given XTextRange. rImport.GetTextImport()->GetText()->insertTextContent(rRange, xTextContent, true); // xml:id for RDF metadata -- after insertion! rImport.SetXmlId(xIfc, i_rXmlId); return xTextContent; } catch (css::lang::IllegalArgumentException &) { OSL_FAIL("CreateAndInsertMark: cannot insert?"); return nullptr; } } } return nullptr; } bool XMLTextMarkImportContext::FindName( const Reference & xAttrList) { bool bNameOK = false; // find name attribute first for( auto& aIter : sax_fastparser::castToFastAttributeList(xAttrList) ) { OUString sValue = aIter.toString(); switch(aIter.getToken()) { case XML_ELEMENT(TEXT, XML_NAME): m_sBookmarkName = sValue; bNameOK = true; break; case XML_ELEMENT(XML, XML_ID): m_sXmlId = sValue; break; // RDFa case XML_ELEMENT(XHTML, XML_ABOUT): m_sAbout = sValue; m_bHaveAbout = true; break; case XML_ELEMENT(XHTML, XML_PROPERTY): m_sProperty = sValue; break; case XML_ELEMENT(XHTML, XML_CONTENT): m_sContent = sValue; break; case XML_ELEMENT(XHTML, XML_DATATYPE): m_sDatatype = sValue; break; case XML_ELEMENT(FIELD, XML_TYPE): m_sFieldName = sValue; break; case XML_ELEMENT(LO_EXT, XML_HIDDEN): ::sax::Converter::convertBool(m_isHidden, sValue); break; case XML_ELEMENT(LO_EXT, XML_CONDITION): m_sCondition = sValue; break; default: XMLOFF_WARN_UNKNOWN("xmloff", aIter); } } return bNameOK; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */