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 <config_folders.h>
21 #include <sfx2/sfxhelp.hxx>
23 #include <string_view>
28 #include <Foundation/NSString.h>
29 #include <CoreFoundation/CFURL.h>
30 #include <CoreServices/CoreServices.h>
34 #include <sal/log.hxx>
35 #include <com/sun/star/uno/Reference.h>
36 #include <com/sun/star/frame/Desktop.hpp>
37 #include <com/sun/star/frame/UnknownModuleException.hpp>
38 #include <com/sun/star/frame/XFrame2.hpp>
39 #include <comphelper/processfactory.hxx>
40 #include <com/sun/star/awt/XWindow.hpp>
41 #include <com/sun/star/awt/XTopWindow.hpp>
42 #include <com/sun/star/beans/XPropertySet.hpp>
43 #include <com/sun/star/frame/FrameSearchFlag.hpp>
44 #include <toolkit/helper/vclunohelper.hxx>
45 #include <com/sun/star/frame/ModuleManager.hpp>
46 #include <unotools/configmgr.hxx>
47 #include <svtools/helpopt.hxx>
48 #include <unotools/moduleoptions.hxx>
49 #include <tools/urlobj.hxx>
50 #include <ucbhelper/content.hxx>
51 #include <unotools/pathoptions.hxx>
52 #include <rtl/byteseq.hxx>
53 #include <rtl/ustring.hxx>
54 #include <officecfg/Office/Common.hxx>
55 #include <osl/process.h>
56 #include <osl/file.hxx>
57 #include <unotools/tempfile.hxx>
58 #include <unotools/securityoptions.hxx>
59 #include <rtl/uri.hxx>
60 #include <vcl/commandinfoprovider.hxx>
61 #include <vcl/keycod.hxx>
62 #include <vcl/settings.hxx>
63 #include <vcl/waitobj.hxx>
64 #include <vcl/weld.hxx>
65 #include <openuriexternally.hxx>
67 #include <comphelper/lok.hxx>
68 #include <LibreOfficeKit/LibreOfficeKitEnums.h>
69 #include <sfx2/viewsh.hxx>
71 #include "newhelp.hxx"
72 #include <sfx2/flatpak.hxx>
73 #include <sfx2/sfxresid.hxx>
75 #include <sfx2/strings.hrc>
76 #include <vcl/svapp.hxx>
77 #include <rtl/string.hxx>
78 #include <svtools/langtab.hxx>
79 #include <tools/diagnose_ex.h>
81 using namespace ::com::sun::star::beans
;
82 using namespace ::com::sun::star::frame
;
83 using namespace ::com::sun::star::uno
;
84 using namespace ::com::sun::star::util
;
85 using namespace ::com::sun::star::lang
;
92 std::unique_ptr
<weld::MessageDialog
> m_xErrBox
;
94 DECL_STATIC_LINK(NoHelpErrorBox
, HelpRequestHdl
, weld::Widget
&, bool);
96 explicit NoHelpErrorBox(weld::Widget
* pParent
)
97 : m_xErrBox(Application::CreateMessageDialog(pParent
, VclMessageType::Error
, VclButtonsType::Ok
,
98 SfxResId(RID_STR_HLPFILENOTEXIST
)))
100 // Error message: "No help available"
101 m_xErrBox
->connect_help(LINK(nullptr, NoHelpErrorBox
, HelpRequestHdl
));
111 IMPL_STATIC_LINK_NOARG(NoHelpErrorBox
, HelpRequestHdl
, weld::Widget
&, bool)
113 // do nothing, because no help available
117 static OUString
const & HelpLocaleString();
121 /// Root path of the help.
122 OUString
const & getHelpRootURL()
124 static OUString
const s_instURL
= [&]()
126 OUString tmp
= officecfg::Office::Common::Path::Current::Help::get(comphelper::getProcessComponentContext());
129 // try to determine path from default
130 tmp
= "$(instpath)/" LIBO_SHARE_HELP_FOLDER
;
133 // replace anything like $(instpath);
134 SvtPathOptions aOptions
;
135 tmp
= aOptions
.SubstituteVariable(tmp
);
138 if (osl::FileBase::getFileURLFromSystemPath(tmp
, url
) == osl::FileBase::E_None
)
145 bool impl_checkHelpLocalePath(OUString
const & rpPath
)
147 osl::DirectoryItem directoryItem
;
150 osl::FileStatus
fileStatus(osl_FileStatus_Mask_Type
| osl_FileStatus_Mask_FileURL
| osl_FileStatus_Mask_FileName
);
151 if (osl::DirectoryItem::get(rpPath
, directoryItem
) == osl::FileBase::E_None
&&
152 directoryItem
.getFileStatus(fileStatus
) == osl::FileBase::E_None
&&
153 fileStatus
.isDirectory())
160 /// Check for built-in help
161 /// Check if help/<lang>/err.html file exist
162 bool impl_hasHelpInstalled()
164 if (comphelper::LibreOfficeKit::isActive())
167 // detect installed locale
168 static OUString
const aLocaleStr
= HelpLocaleString();
170 OUString helpRootURL
= getHelpRootURL() + "/" + aLocaleStr
+ "/err.html";
172 osl::DirectoryItem directoryItem
;
173 if(osl::DirectoryItem::get(helpRootURL
, directoryItem
) == osl::FileBase::E_None
){
177 SAL_INFO( "sfx.appl", "Checking old help installed " << bOK
);
181 /// Check for html built-in help
182 /// Check if help/lang/text folder exist. Only html has it.
183 bool impl_hasHTMLHelpInstalled()
185 if (comphelper::LibreOfficeKit::isActive())
188 // detect installed locale
189 static OUString
const aLocaleStr
= HelpLocaleString();
191 OUString helpRootURL
= getHelpRootURL() + "/" + aLocaleStr
+ "/text";
192 bool bOK
= impl_checkHelpLocalePath( helpRootURL
);
193 SAL_INFO( "sfx.appl", "Checking new help (html) installed " << bOK
);
199 /// Return the locale we prefer for displaying help
200 static OUString
const & HelpLocaleString()
202 if (comphelper::LibreOfficeKit::isActive())
203 return comphelper::LibreOfficeKit::getLanguageTag().getBcp47();
205 static OUString aLocaleStr
;
206 if (!aLocaleStr
.isEmpty())
209 const OUString
aEnglish("en-US");
210 // detect installed locale
211 aLocaleStr
= utl::ConfigManager::getUILocale();
213 if ( aLocaleStr
.isEmpty() )
215 aLocaleStr
= aEnglish
;
219 // get fall-back language (country)
220 OUString sLang
= aLocaleStr
;
221 sal_Int32 nSepPos
= sLang
.indexOf( '-' );
224 sLang
= sLang
.copy( 0, nSepPos
);
226 OUString
sHelpPath("");
227 sHelpPath
= getHelpRootURL() + "/" + utl::ConfigManager::getProductVersion() + "/" + aLocaleStr
;
228 if (impl_checkHelpLocalePath(sHelpPath
))
232 sHelpPath
= getHelpRootURL() + "/" + utl::ConfigManager::getProductVersion() + "/" + sLang
;
233 if (impl_checkHelpLocalePath(sHelpPath
))
238 sHelpPath
= getHelpRootURL() + "/" + aLocaleStr
;
239 if (impl_checkHelpLocalePath(sHelpPath
))
243 sHelpPath
= getHelpRootURL() + "/" + sLang
;
244 if (impl_checkHelpLocalePath(sHelpPath
))
249 sHelpPath
= getHelpRootURL() + "/" + utl::ConfigManager::getProductVersion() + "/" + aEnglish
;
250 if (impl_checkHelpLocalePath(sHelpPath
))
252 aLocaleStr
= aEnglish
;
255 sHelpPath
= getHelpRootURL() + "/" + aEnglish
;
256 if (impl_checkHelpLocalePath(sHelpPath
))
258 aLocaleStr
= aEnglish
;
266 void AppendConfigToken( OUStringBuffer
& rURL
, bool bQuestionMark
)
268 OUString aLocaleStr
= HelpLocaleString();
270 // query part exists?
272 // no, so start with '?'
275 // yes, so only append with '&'
279 rURL
.append("Language=");
280 rURL
.append(aLocaleStr
);
281 rURL
.append("&System=");
282 rURL
.append(SvtHelpOptions().GetSystem());
283 rURL
.append("&Version=");
284 rURL
.append(utl::ConfigManager::getProductVersion());
287 static bool GetHelpAnchor_Impl( const OUString
& _rURL
, OUString
& _rAnchor
)
293 ::ucbhelper::Content
aCnt( INetURLObject( _rURL
).GetMainURL( INetURLObject::DecodeMechanism::NONE
),
294 Reference
< css::ucb::XCommandEnvironment
>(),
295 comphelper::getProcessComponentContext() );
297 if ( aCnt
.getPropertyValue("AnchorName") >>= sAnchor
)
300 if ( !sAnchor
.isEmpty() )
308 SAL_WARN( "sfx.appl", "Property 'AnchorName' is missing" );
311 catch (const css::uno::Exception
&)
323 static OUString
GetHelpText( const OUString
& aCommandURL
, const OUString
& rModule
);
328 OUString
SfxHelp_Impl::GetHelpText( const OUString
& aCommandURL
, const OUString
& rModule
)
331 OUStringBuffer
aHelpURL( SfxHelp::CreateHelpURL( aCommandURL
, rModule
) );
332 // added 'active' parameter
333 sal_Int32 nIndex
= aHelpURL
.lastIndexOf( '#' );
335 nIndex
= aHelpURL
.getLength();
336 aHelpURL
.insert( nIndex
, "&Active=true" );
338 return SfxContentHelper::GetActiveHelpString( aHelpURL
.makeStringAndClear() );
343 , bLaunchingHelp(false)
345 // read the environment variable "HELP_DEBUG"
346 // if it's set, you will see debug output on active help
348 OUString
sEnvVarName( "HELP_DEBUG" );
349 osl_getEnvironment( sEnvVarName
.pData
, &sHelpDebug
.pData
);
350 bIsDebug
= !sHelpDebug
.isEmpty();
357 static OUString
getDefaultModule_Impl()
359 OUString sDefaultModule
;
360 SvtModuleOptions aModOpt
;
361 if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::WRITER
) )
362 sDefaultModule
= "swriter";
363 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::CALC
) )
364 sDefaultModule
= "scalc";
365 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::IMPRESS
) )
366 sDefaultModule
= "simpress";
367 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::DRAW
) )
368 sDefaultModule
= "sdraw";
369 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::MATH
) )
370 sDefaultModule
= "smath";
371 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::CHART
) )
372 sDefaultModule
= "schart";
373 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::BASIC
) )
374 sDefaultModule
= "sbasic";
375 else if ( aModOpt
.IsModuleInstalled( SvtModuleOptions::EModule::DATABASE
) )
376 sDefaultModule
= "sdatabase";
379 SAL_WARN( "sfx.appl", "getDefaultModule_Impl(): no module installed" );
381 return sDefaultModule
;
384 static OUString
getCurrentModuleIdentifier_Impl()
386 OUString sIdentifier
;
387 Reference
< XComponentContext
> xContext
= ::comphelper::getProcessComponentContext();
388 Reference
< XModuleManager2
> xModuleManager
= ModuleManager::create(xContext
);
389 Reference
< XDesktop2
> xDesktop
= Desktop::create(xContext
);
390 Reference
< XFrame
> xCurrentFrame
= xDesktop
->getCurrentFrame();
392 if ( xCurrentFrame
.is() )
396 sIdentifier
= xModuleManager
->identify( xCurrentFrame
);
398 catch (const css::frame::UnknownModuleException
&)
400 SAL_INFO( "sfx.appl", "SfxHelp::getCurrentModuleIdentifier_Impl(): unknown module (help in help?)" );
402 catch (const Exception
&)
404 TOOLS_WARN_EXCEPTION( "sfx.appl", "SfxHelp::getCurrentModuleIdentifier_Impl(): exception of XModuleManager::identify()" );
413 OUString
MapModuleIdentifier(const OUString
&rFactoryShortName
)
415 OUString
aFactoryShortName(rFactoryShortName
);
417 // Map some module identifiers to their "real" help module string.
418 if ( aFactoryShortName
== "chart2" )
419 aFactoryShortName
= "schart" ;
420 else if ( aFactoryShortName
== "BasicIDE" )
421 aFactoryShortName
= "sbasic";
422 else if ( aFactoryShortName
== "sweb"
423 || aFactoryShortName
== "sglobal"
424 || aFactoryShortName
== "swxform" )
425 aFactoryShortName
= "swriter" ;
426 else if ( aFactoryShortName
== "dbquery"
427 || aFactoryShortName
== "dbbrowser"
428 || aFactoryShortName
== "dbrelation"
429 || aFactoryShortName
== "dbtable"
430 || aFactoryShortName
== "dbapp"
431 || aFactoryShortName
== "dbreport"
432 || aFactoryShortName
== "dbtdata"
433 || aFactoryShortName
== "swreport"
434 || aFactoryShortName
== "swform" )
435 aFactoryShortName
= "sdatabase";
436 else if ( aFactoryShortName
== "sbibliography"
437 || aFactoryShortName
== "sabpilot"
438 || aFactoryShortName
== "scanner"
439 || aFactoryShortName
== "spropctrlr"
440 || aFactoryShortName
== "StartModule" )
441 aFactoryShortName
.clear();
443 return aFactoryShortName
;
447 OUString
SfxHelp::GetHelpModuleName_Impl(const OUString
& rHelpID
)
449 OUString aFactoryShortName
;
451 //rhbz#1438876 detect preferred module for this help id, e.g. csv dialog
452 //for calc import before any toplevel is created and so context is
453 //otherwise unknown. Cosmetic, same help is shown in any case because its
454 //in the shared section, but title bar would state "Writer" when context is
455 //expected to be "Calc"
457 if (rHelpID
.startsWith("modules/", &sRemainder
))
459 sal_Int32 nEndModule
= sRemainder
.indexOf('/');
460 aFactoryShortName
= nEndModule
!= -1 ? sRemainder
.copy(0, nEndModule
) : sRemainder
;
463 if (aFactoryShortName
.isEmpty())
465 OUString aModuleIdentifier
= getCurrentModuleIdentifier_Impl();
466 if (!aModuleIdentifier
.isEmpty())
470 Reference
< XModuleManager2
> xModuleManager(
471 ModuleManager::create(::comphelper::getProcessComponentContext()) );
472 Sequence
< PropertyValue
> lProps
;
473 xModuleManager
->getByName( aModuleIdentifier
) >>= lProps
;
474 auto pProp
= std::find_if(lProps
.begin(), lProps
.end(),
475 [](const PropertyValue
& rProp
) { return rProp
.Name
== "ooSetupFactoryShortName"; });
476 if (pProp
!= lProps
.end())
477 pProp
->Value
>>= aFactoryShortName
;
479 catch (const Exception
&)
481 TOOLS_WARN_EXCEPTION( "sfx.appl", "SfxHelp::GetHelpModuleName_Impl()" );
486 if (!aFactoryShortName
.isEmpty())
487 aFactoryShortName
= MapModuleIdentifier(aFactoryShortName
);
488 if (aFactoryShortName
.isEmpty())
489 aFactoryShortName
= getDefaultModule_Impl();
491 return aFactoryShortName
;
494 OUString
SfxHelp::CreateHelpURL_Impl( const OUString
& aCommandURL
, const OUString
& rModuleName
)
496 // build up the help URL
497 OUStringBuffer
aHelpURL("vnd.sun.star.help://");
498 bool bHasAnchor
= false;
501 OUString
aModuleName( rModuleName
);
502 if (aModuleName
.isEmpty())
503 aModuleName
= getDefaultModule_Impl();
505 aHelpURL
.append(aModuleName
);
507 if ( aCommandURL
.isEmpty() )
508 aHelpURL
.append("/start");
511 aHelpURL
.append('/');
512 aHelpURL
.append(rtl::Uri::encode(aCommandURL
,
513 rtl_UriCharClassRelSegment
,
514 rtl_UriEncodeKeepEscapes
,
515 RTL_TEXTENCODING_UTF8
));
517 OUStringBuffer aTempURL
= aHelpURL
;
518 AppendConfigToken( aTempURL
, true );
519 bHasAnchor
= GetHelpAnchor_Impl(aTempURL
.makeStringAndClear(), aAnchor
);
522 AppendConfigToken( aHelpURL
, true );
526 aHelpURL
.append('#');
527 aHelpURL
.append(aAnchor
);
530 return aHelpURL
.makeStringAndClear();
533 static SfxHelpWindow_Impl
* impl_createHelp(Reference
< XFrame2
>& rHelpTask
,
534 Reference
< XFrame
>& rHelpContent
)
536 Reference
< XDesktop2
> xDesktop
= Desktop::create( ::comphelper::getProcessComponentContext() );
538 // otherwise - create new help task
539 Reference
< XFrame2
> xHelpTask(
540 xDesktop
->findFrame( "OFFICE_HELP_TASK", FrameSearchFlag::TASKS
| FrameSearchFlag::CREATE
),
545 // create all internal windows and sub frames ...
546 Reference
< css::awt::XWindow
> xParentWindow
= xHelpTask
->getContainerWindow();
547 VclPtr
<vcl::Window
> pParentWindow
= VCLUnoHelper::GetWindow( xParentWindow
);
548 VclPtrInstance
<SfxHelpWindow_Impl
> pHelpWindow( xHelpTask
, pParentWindow
);
549 Reference
< css::awt::XWindow
> xHelpWindow
= VCLUnoHelper::GetInterface( pHelpWindow
);
551 Reference
< XFrame
> xHelpContent
;
552 if (xHelpTask
->setComponent( xHelpWindow
, Reference
< XController
>() ))
555 xHelpTask
->setName("OFFICE_HELP_TASK");
557 Reference
< XPropertySet
> xProps(xHelpTask
, UNO_QUERY
);
559 xProps
->setPropertyValue(
561 makeAny(SfxResId(STR_HELP_WINDOW_TITLE
)));
563 pHelpWindow
->setContainerWindow( xParentWindow
);
564 xParentWindow
->setVisible(true);
565 xHelpWindow
->setVisible(true);
567 // This sub frame is created internally (if we called new SfxHelpWindow_Impl() ...)
568 // It should exist :-)
569 xHelpContent
= xHelpTask
->findFrame("OFFICE_HELP", FrameSearchFlag::CHILDREN
);
572 if (!xHelpContent
.is())
574 pHelpWindow
.disposeAndClear();
578 xHelpContent
->setName("OFFICE_HELP");
580 rHelpTask
= xHelpTask
;
581 rHelpContent
= xHelpContent
;
585 OUString
SfxHelp::GetHelpText( const OUString
& aCommandURL
, const vcl::Window
* pWindow
)
587 OUString sModuleName
= GetHelpModuleName_Impl(aCommandURL
);
588 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(aCommandURL
, getCurrentModuleIdentifier_Impl());
589 OUString sRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
590 OUString sHelpText
= SfxHelp_Impl::GetHelpText( sRealCommand
.isEmpty() ? aCommandURL
: sRealCommand
, sModuleName
);
594 if (pWindow
&& sHelpText
.isEmpty())
596 // no help text found -> try with parent help id.
597 vcl::Window
* pParent
= pWindow
->GetParent();
600 aNewHelpId
= pParent
->GetHelpId();
601 sHelpText
= SfxHelp_Impl::GetHelpText( OStringToOUString(aNewHelpId
, RTL_TEXTENCODING_UTF8
), sModuleName
);
602 if (!sHelpText
.isEmpty())
605 pParent
= pParent
->GetParent();
608 if (bIsDebug
&& sHelpText
.isEmpty())
612 // add some debug information?
615 sHelpText
+= "\n-------------\n" +
616 sModuleName
+ ": " + aCommandURL
;
617 if ( !aNewHelpId
.isEmpty() )
620 OStringToOUString(aNewHelpId
, RTL_TEXTENCODING_UTF8
);
627 OUString
SfxHelp::GetHelpText(const OUString
& aCommandURL
, const weld::Widget
* pWidget
)
629 OUString sModuleName
= GetHelpModuleName_Impl(aCommandURL
);
630 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(aCommandURL
, getCurrentModuleIdentifier_Impl());
631 OUString sRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
632 OUString sHelpText
= SfxHelp_Impl::GetHelpText( sRealCommand
.isEmpty() ? aCommandURL
: sRealCommand
, sModuleName
);
636 if (pWidget
&& sHelpText
.isEmpty())
638 // no help text found -> try with parent help id.
639 std::unique_ptr
<weld::Widget
> xParent(pWidget
->weld_parent());
642 aNewHelpId
= xParent
->get_help_id();
643 sHelpText
= SfxHelp_Impl::GetHelpText( OStringToOUString(aNewHelpId
, RTL_TEXTENCODING_UTF8
), sModuleName
);
644 if (!sHelpText
.isEmpty())
647 xParent
= xParent
->weld_parent();
650 if (bIsDebug
&& sHelpText
.isEmpty())
654 // add some debug information?
657 sHelpText
+= "\n-------------\n" +
658 sModuleName
+ ": " + aCommandURL
;
659 if ( !aNewHelpId
.isEmpty() )
662 OStringToOUString(aNewHelpId
, RTL_TEXTENCODING_UTF8
);
669 OUString
SfxHelp::GetURLHelpText(std::u16string_view aURL
)
671 SvtSecurityOptions aSecOpt
;
672 bool bCtrlClickHlink
= aSecOpt
.IsOptionSet(SvtSecurityOptions::EOption::CtrlClickHyperlink
);
674 // "ctrl-click to follow link:" for not MacOS
675 // "⌘-click to follow link:" for MacOs
676 vcl::KeyCode
aCode(KEY_SPACE
);
677 vcl::KeyCode
aModifiedCode(KEY_SPACE
, KEY_MOD1
);
678 OUString
aModStr(aModifiedCode
.GetName());
679 aModStr
= aModStr
.replaceFirst(aCode
.GetName(), "");
680 aModStr
= aModStr
.replaceAll("+", "");
682 = bCtrlClickHlink
? SfxResId(STR_CTRLCLICKHYPERLINK
) : SfxResId(STR_CLICKHYPERLINK
);
683 aHelpStr
= aHelpStr
.replaceFirst("%{key}", aModStr
);
684 aHelpStr
= aHelpStr
.replaceFirst("%{link}", aURL
);
688 void SfxHelp::SearchKeyword( const OUString
& rKeyword
)
690 Start_Impl(OUString(), static_cast<vcl::Window
*>(nullptr), rKeyword
);
693 bool SfxHelp::Start( const OUString
& rURL
, const vcl::Window
* pWindow
)
697 bLaunchingHelp
= true;
698 bool bRet
= Start_Impl( rURL
, pWindow
, OUString() );
699 bLaunchingHelp
= false;
703 bool SfxHelp::Start(const OUString
& rURL
, weld::Widget
* pWidget
)
707 bLaunchingHelp
= true;
708 bool bRet
= Start_Impl(rURL
, pWidget
, OUString());
709 bLaunchingHelp
= false;
713 /// Redirect the vnd.sun.star.help:// urls to http://help.libreoffice.org
714 static bool impl_showOnlineHelp( const OUString
& rURL
)
716 static constexpr OUStringLiteral
aInternal(u
"vnd.sun.star.help://");
717 if ( rURL
.getLength() <= aInternal
.getLength() || !rURL
.startsWith(aInternal
) )
720 OUString aHelpLink
= officecfg::Office::Common::Help::HelpRootURL::get();
721 OUString aTarget
= OUString::Concat("Target=") + rURL
.subView(aInternal
.getLength());
722 aTarget
= aTarget
.replaceAll("%2F", "/").replaceAll("?", "&");
723 aHelpLink
+= aTarget
;
725 if (comphelper::LibreOfficeKit::isActive())
727 if(SfxViewShell
* pViewShell
= SfxViewShell::Current())
729 pViewShell
->libreOfficeKitViewCallback(LOK_CALLBACK_HYPERLINK_CLICKED
,
730 aHelpLink
.toUtf8().getStr());
739 LSOpenCFURLRef(CFURLCreateWithString(kCFAllocatorDefault
,
740 CFStringCreateWithCString(kCFAllocatorDefault
,
741 aHelpLink
.toUtf8().getStr(),
742 kCFStringEncodingUTF8
),
746 sfx2::openUriExternally(aHelpLink
, false);
750 catch (const Exception
&)
758 bool rewriteFlatpakHelpRootUrl(OUString
* helpRootUrl
) {
759 assert(helpRootUrl
!= nullptr);
760 //TODO: this function for now assumes that the passed-in *helpRootUrl references
761 // /app/libreoffice/help (which belongs to the org.libreoffice.LibreOffice.Help
762 // extension); it replaces it with the corresponding file URL as seen outside the flatpak
764 struct Failure
: public std::exception
{};
766 static auto const url
= [] {
767 // From /.flatpak-info [Instance] section, read
769 // app-extensions=...;org.libreoffice.LibreOffice.Help=<sha>;...
771 osl::File
ini("file:///.flatpak-info");
772 auto err
= ini
.open(osl_File_OpenFlag_Read
);
773 if (err
!= osl::FileBase::E_None
) {
774 SAL_WARN("sfx.appl", "LIBO_FLATPAK mode failure opening /.flatpak-info: " << err
);
779 bool havePath
= false;
780 bool haveExtensions
= false;
781 for (bool instance
= false; !(havePath
&& haveExtensions
);) {
782 rtl::ByteSequence bytes
;
783 err
= ini
.readLine(bytes
);
784 if (err
!= osl::FileBase::E_None
) {
787 "LIBO_FLATPAK mode reading /.flatpak-info fails with " << err
788 << " before [Instance] app-path");
791 std::string_view
const line(
792 reinterpret_cast<char const *>(bytes
.getConstArray()), bytes
.getLength());
794 static constexpr auto keyPath
= std::string_view("app-path=");
795 static constexpr auto keyExtensions
= std::string_view("app-extensions=");
796 if (!havePath
&& line
.length() >= keyPath
.size()
797 && line
.substr(0, keyPath
.size()) == keyPath
.data())
799 auto const value
= line
.substr(keyPath
.size());
800 if (!rtl_convertStringToUString(
801 &path
.pData
, value
.data(), value
.length(),
802 osl_getThreadTextEncoding(),
803 (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_ERROR
804 | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_ERROR
805 | RTL_TEXTTOUNICODE_FLAGS_INVALID_ERROR
)))
809 "LIBO_FLATPAK mode failure converting app-path \"" << value
814 } else if (!haveExtensions
&& line
.length() >= keyExtensions
.size()
815 && line
.substr(0, keyExtensions
.size()) == keyExtensions
.data())
817 auto const value
= line
.substr(keyExtensions
.size());
818 if (!rtl_convertStringToUString(
819 &extensions
.pData
, value
.data(), value
.length(),
820 osl_getThreadTextEncoding(),
821 (RTL_TEXTTOUNICODE_FLAGS_UNDEFINED_ERROR
822 | RTL_TEXTTOUNICODE_FLAGS_MBUNDEFINED_ERROR
823 | RTL_TEXTTOUNICODE_FLAGS_INVALID_ERROR
)))
827 "LIBO_FLATPAK mode failure converting app-extensions \"" << value
831 haveExtensions
= true;
832 } else if (line
.length() > 0 && line
[0] == '[') {
835 "LIBO_FLATPAK mode /.flatpak-info lacks [Instance] app-path and"
839 } else if (line
== "[Instance]") {
844 // Extract <sha> from ...;org.libreoffice.LibreOffice.Help=<sha>;...:
846 for (sal_Int32 i
= 0;;) {
847 OUString elem
= extensions
.getToken(0, ';', i
);
848 if (elem
.startsWith("org.libreoffice.LibreOffice.Help=", &sha
)) {
854 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-extensions \""
855 << extensions
<< "\" org.libreoffice.LibreOffice.Help");
859 // Assuming that <path> is of the form
860 // /.../app/org.libreoffice.LibreOffice/<arch>/<branch>/<sha'>/files
862 // /.../runtime/org.libreoffice.LibreOffice.Help/<arch>/<branch>/<sha>/files
863 // because the extension's files are stored at a different place than the app's files,
864 // so use this hack until flatpak itself provides a better solution:
865 static constexpr auto segments
= OUStringLiteral(u
"/app/org.libreoffice.LibreOffice/");
866 auto const i1
= path
.lastIndexOf(segments
);
867 // use lastIndexOf instead of indexOf, in case the user-controlled prefix /.../
868 // happens to contain such segments
872 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
873 << "\" doesn't contain /app/org.libreoffice.LibreOffice/");
876 auto const i2
= i1
+ segments
.getLength();
877 auto i3
= path
.indexOf('/', i2
);
881 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
882 << "\" doesn't contain branch segment");
885 i3
= path
.indexOf('/', i3
+ 1);
889 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
890 << "\" doesn't contain sha segment");
894 auto const i4
= path
.indexOf('/', i3
);
898 "LIBO_FLATPAK mode /.flatpak-info [Instance] app-path \"" << path
899 << "\" doesn't contain files segment");
902 path
= path
.subView(0, i1
) + OUString::Concat("/runtime/org.libreoffice.LibreOffice.Help/")
903 + path
.subView(i2
, i3
- i2
) + sha
+ path
.subView(i4
);
904 // Turn <path> into a file URL:
906 err
= osl::FileBase::getFileURLFromSystemPath(path
, url_
);
907 if (err
!= osl::FileBase::E_None
) {
910 "LIBO_FLATPAK mode failure converting app-path \"" << path
<< "\" to URL: "
918 } catch (Failure
&) {
925 // add <noscript> meta for browsers without javascript
927 #define SHTML1 "<!DOCTYPE HTML><html lang=\"en-US\"><head><meta charset=\"UTF-8\">"
928 #define SHTML2 "<noscript><meta http-equiv=\"refresh\" content=\"0; url='"
929 #define SHTML3 "/noscript.html'\"></noscript><meta http-equiv=\"refresh\" content=\"1; url='"
930 #define SHTML4 "'\"><script type=\"text/javascript\"> window.location.href = \""
931 #define SHTML5 "\";</script><title>Help Page Redirection</title></head><body></body></html>"
933 // use a tempfile since e.g. xdg-open doesn't support URL-parameters with file:// URLs
934 static bool impl_showOfflineHelp( const OUString
& rURL
)
936 OUString aBaseInstallPath
= getHelpRootURL();
937 // For the flatpak case, find the pathname outside the flatpak sandbox that corresponds to
938 // aBaseInstallPath, because that is what needs to be stored in aTempFile below:
939 if (flatpak::isFlatpak() && !rewriteFlatpakHelpRootUrl(&aBaseInstallPath
)) {
943 OUString
aHelpLink( aBaseInstallPath
+ "/index.html?" );
944 OUString aTarget
= OUString::Concat("Target=") + rURL
.subView(RTL_CONSTASCII_LENGTH("vnd.sun.star.help://"));
945 aTarget
= aTarget
.replaceAll("%2F","/").replaceAll("?","&");
946 aHelpLink
+= aTarget
;
948 // Get a html tempfile (for the flatpak case, create it in XDG_CACHE_HOME instead of /tmp for
949 // technical reasons, so that it can be accessed by the browser running outside the sandbox):
950 OUString
const aExtension(".html");
951 OUString
* parent
= nullptr;
952 if (flatpak::isFlatpak() && !flatpak::createTemporaryHtmlDirectory(&parent
)) {
955 ::utl::TempFile
aTempFile("NewHelp", true, &aExtension
, parent
, false );
957 SvStream
* pStream
= aTempFile
.GetStream(StreamMode::WRITE
);
958 pStream
->SetStreamCharSet(RTL_TEXTENCODING_UTF8
);
960 OUString aTempStr
= SHTML1 SHTML2
+
961 aBaseInstallPath
+ "/" + HelpLocaleString() + SHTML3
+
965 pStream
->WriteUnicodeOrByteText(aTempStr
);
967 aTempFile
.CloseStream();
971 LSOpenCFURLRef(CFURLCreateWithString(kCFAllocatorDefault
,
972 CFStringCreateWithCString(kCFAllocatorDefault
,
973 aTempFile
.GetURL().toUtf8().getStr(),
974 kCFStringEncodingUTF8
),
978 sfx2::openUriExternally(aTempFile
.GetURL(), false);
982 catch (const Exception
&)
985 aTempFile
.EnableKillingFile();
991 // tdf#119579 skip floating windows as potential parent for missing help dialog
992 const vcl::Window
* GetBestParent(const vcl::Window
* pWindow
)
996 if (pWindow
->IsSystemWindow() && pWindow
->GetType() != WindowType::FLOATINGWINDOW
)
998 pWindow
= pWindow
->GetParent();
1006 class HelpManualMessage
: public weld::MessageDialogController
1009 std::unique_ptr
<weld::CheckButton
> m_xHideOfflineHelpCB
;
1012 HelpManualMessage(weld::Widget
* pParent
)
1013 : MessageDialogController(pParent
, "sfx/ui/helpmanual.ui", "onlinehelpmanual", "hidedialog")
1014 , m_xHideOfflineHelpCB(m_xBuilder
->weld_check_button("hidedialog"))
1016 LanguageTag aLangTag
= Application::GetSettings().GetUILanguageTag();
1017 OUString sLocaleString
= SvtLanguageTable::GetLanguageString(aLangTag
.getLanguageType());
1018 OUString sPrimText
= get_primary_text();
1019 set_primary_text(sPrimText
.replaceAll("$UILOCALE", sLocaleString
));
1022 bool GetOfflineHelpPopUp() const { return !m_xHideOfflineHelpCB
->get_active(); }
1027 bool SfxHelp::Start_Impl(const OUString
& rURL
, const vcl::Window
* pWindow
, const OUString
& rKeyword
)
1029 OUStringBuffer
aHelpRootURL("vnd.sun.star.help://");
1030 AppendConfigToken(aHelpRootURL
, true);
1031 SfxContentHelper::GetResultSet(aHelpRootURL
.makeStringAndClear());
1035 * - a HelpID (formerly a long, now a string)
1036 * If rURL is a URL, CreateHelpURL should be called for this URL
1037 * If rURL is an arbitrary string, the same should happen, but the URL should be tried out
1038 * if it delivers real help content. In case only the Help Error Document is returned, the
1039 * parent of the window for that help was called, is asked for its HelpID.
1040 * For compatibility reasons this upward search is not implemented for "real" URLs.
1041 * Help keyword search now is implemented as own method; in former versions it
1042 * was done via Help::Start, but this implementation conflicted with the upward search.
1045 INetURLObject
aParser( rURL
);
1046 INetProtocol nProtocol
= aParser
.GetProtocol();
1048 switch ( nProtocol
)
1050 case INetProtocol::VndSunStarHelp
:
1051 // already a vnd.sun.star.help URL -> nothing to do
1056 OUString
aHelpModuleName(GetHelpModuleName_Impl(rURL
));
1057 OUString aRealCommand
;
1059 if ( nProtocol
== INetProtocol::Uno
)
1061 // Command can be just an alias to another command.
1062 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(rURL
, getCurrentModuleIdentifier_Impl());
1063 aRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
1066 // no URL, just a HelpID (maybe empty in case of keyword search)
1067 aHelpURL
= CreateHelpURL_Impl( aRealCommand
.isEmpty() ? rURL
: aRealCommand
, aHelpModuleName
);
1069 if ( impl_hasHelpInstalled() && pWindow
&& SfxContentHelper::IsHelpErrorDocument( aHelpURL
) )
1071 // no help found -> try with parent help id.
1072 vcl::Window
* pParent
= pWindow
->GetParent();
1075 OString aHelpId
= pParent
->GetHelpId();
1076 aHelpURL
= CreateHelpURL( OStringToOUString(aHelpId
, RTL_TEXTENCODING_UTF8
), aHelpModuleName
);
1078 if ( !SfxContentHelper::IsHelpErrorDocument( aHelpURL
) )
1084 pParent
= pParent
->GetParent();
1087 // create help url of start page ( helpid == 0 -> start page)
1088 aHelpURL
= CreateHelpURL( OUString(), aHelpModuleName
);
1097 if ( comphelper::LibreOfficeKit::isActive() )
1099 impl_showOnlineHelp( aHelpURL
);
1103 if (@
available(macOS
10.14, *)) {
1104 // Workaround: Safari sandboxing prevents it from accessing files in the LibreOffice.app folder
1105 // force online-help instead if Safari is default browser.
1106 CFURLRef pBrowser
= LSCopyDefaultApplicationURLForURL(
1107 CFURLCreateWithString(
1108 kCFAllocatorDefault
,
1109 static_cast<CFStringRef
>(@
"https://www.libreoffice.org"),
1111 kLSRolesAll
, nullptr);
1112 if([static_cast<NSString
*>(CFURLGetString(pBrowser
)) isEqualToString
:@
"file:///Applications/Safari.app/"]) {
1113 impl_showOnlineHelp( aHelpURL
);
1119 // If the HTML or no help is installed, but aHelpURL nevertheless references valid help content,
1120 // that implies that this help content belongs to an extension (and thus would not be available
1121 // in neither the offline nor online HTML help); in that case, fall through to the "old-help to
1122 // display" code below:
1123 if (SfxContentHelper::IsHelpErrorDocument(aHelpURL
))
1125 if ( impl_hasHTMLHelpInstalled() && impl_showOfflineHelp(aHelpURL
) )
1130 if ( !impl_hasHelpInstalled() )
1132 SvtHelpOptions aHelpOptions
;
1133 bool bShowOfflineHelpPopUp
= aHelpOptions
.IsOfflineHelpPopUp();
1135 pWindow
= GetBestParent(pWindow
);
1137 TopLevelWindowLocker aBusy
;
1139 if(bShowOfflineHelpPopUp
)
1141 weld::Window
* pWeldWindow
= pWindow
? pWindow
->GetFrameWeld() : nullptr;
1142 aBusy
.incBusy(pWeldWindow
);
1143 HelpManualMessage
aQueryBox(pWeldWindow
);
1144 short OnlineHelpBox
= aQueryBox
.run();
1145 bShowOfflineHelpPopUp
= OnlineHelpBox
!= RET_OK
;
1146 aHelpOptions
.SetOfflineHelpPopUp(aQueryBox
.GetOfflineHelpPopUp());
1149 if(!bShowOfflineHelpPopUp
)
1151 if ( impl_showOnlineHelp( aHelpURL
) )
1155 weld::Window
* pWeldWindow
= pWindow
? pWindow
->GetFrameWeld() : nullptr;
1156 aBusy
.incBusy(pWeldWindow
);
1157 NoHelpErrorBox
aErrBox(pWeldWindow
);
1170 // old-help to display
1171 Reference
< XDesktop2
> xDesktop
= Desktop::create( ::comphelper::getProcessComponentContext() );
1173 // check if help window is still open
1174 // If not, create a new one and return access directly to the internal sub frame showing the help content
1175 // search must be done here; search one desktop level could return an arbitrary frame
1176 Reference
< XFrame2
> xHelp(
1177 xDesktop
->findFrame( "OFFICE_HELP_TASK", FrameSearchFlag::CHILDREN
),
1179 Reference
< XFrame
> xHelpContent
= xDesktop
->findFrame(
1181 FrameSearchFlag::CHILDREN
);
1183 SfxHelpWindow_Impl
* pHelpWindow
= nullptr;
1185 pHelpWindow
= impl_createHelp(xHelp
, xHelpContent
);
1187 pHelpWindow
= static_cast<SfxHelpWindow_Impl
*>(VCLUnoHelper::GetWindow(xHelp
->getComponentWindow()).get());
1188 if (!xHelp
.is() || !xHelpContent
.is() || !pHelpWindow
)
1191 SAL_INFO("sfx.appl", "HelpId = " << aHelpURL
);
1193 pHelpWindow
->SetHelpURL( aHelpURL
);
1194 pHelpWindow
->loadHelpContent(aHelpURL
);
1195 if (!rKeyword
.isEmpty())
1196 pHelpWindow
->OpenKeyword( rKeyword
);
1198 Reference
< css::awt::XTopWindow
> xTopWindow( xHelp
->getContainerWindow(), UNO_QUERY
);
1199 if ( xTopWindow
.is() )
1200 xTopWindow
->toFront();
1205 bool SfxHelp::Start_Impl(const OUString
& rURL
, weld::Widget
* pWidget
, const OUString
& rKeyword
)
1207 OUStringBuffer
aHelpRootURL("vnd.sun.star.help://");
1208 AppendConfigToken(aHelpRootURL
, true);
1209 SfxContentHelper::GetResultSet(aHelpRootURL
.makeStringAndClear());
1213 * - a HelpID (formerly a long, now a string)
1214 * If rURL is a URL, CreateHelpURL should be called for this URL
1215 * If rURL is an arbitrary string, the same should happen, but the URL should be tried out
1216 * if it delivers real help content. In case only the Help Error Document is returned, the
1217 * parent of the window for that help was called, is asked for its HelpID.
1218 * For compatibility reasons this upward search is not implemented for "real" URLs.
1219 * Help keyword search now is implemented as own method; in former versions it
1220 * was done via Help::Start, but this implementation conflicted with the upward search.
1223 INetURLObject
aParser( rURL
);
1224 INetProtocol nProtocol
= aParser
.GetProtocol();
1226 switch ( nProtocol
)
1228 case INetProtocol::VndSunStarHelp
:
1229 // already a vnd.sun.star.help URL -> nothing to do
1234 OUString
aHelpModuleName(GetHelpModuleName_Impl(rURL
));
1235 OUString aRealCommand
;
1237 if ( nProtocol
== INetProtocol::Uno
)
1239 // Command can be just an alias to another command.
1240 auto aProperties
= vcl::CommandInfoProvider::GetCommandProperties(rURL
, getCurrentModuleIdentifier_Impl());
1241 aRealCommand
= vcl::CommandInfoProvider::GetRealCommandForCommand(aProperties
);
1244 // no URL, just a HelpID (maybe empty in case of keyword search)
1245 aHelpURL
= CreateHelpURL_Impl( aRealCommand
.isEmpty() ? rURL
: aRealCommand
, aHelpModuleName
);
1247 if ( impl_hasHelpInstalled() && pWidget
&& SfxContentHelper::IsHelpErrorDocument( aHelpURL
) )
1249 bool bUseFinalFallback
= true;
1250 // no help found -> try ids of parents.
1251 pWidget
->help_hierarchy_foreach([&aHelpModuleName
, &aHelpURL
, &bUseFinalFallback
](const OString
& rHelpId
){
1252 if (rHelpId
.isEmpty())
1254 aHelpURL
= CreateHelpURL( OStringToOUString(rHelpId
, RTL_TEXTENCODING_UTF8
), aHelpModuleName
);
1255 bool bFinished
= !SfxContentHelper::IsHelpErrorDocument(aHelpURL
);
1257 bUseFinalFallback
= false;
1261 if (bUseFinalFallback
)
1263 // create help url of start page ( helpid == 0 -> start page)
1264 aHelpURL
= CreateHelpURL( OUString(), aHelpModuleName
);
1271 if ( comphelper::LibreOfficeKit::isActive() )
1273 impl_showOnlineHelp( aHelpURL
);
1277 // If the HTML or no help is installed, but aHelpURL nevertheless references valid help content,
1278 // that implies that help content belongs to an extension (and thus would not be available
1279 // in neither the offline nor online HTML help); in that case, fall through to the "old-help to
1280 // display" code below:
1281 if (SfxContentHelper::IsHelpErrorDocument(aHelpURL
))
1283 if ( impl_hasHTMLHelpInstalled() && impl_showOfflineHelp(aHelpURL
) )
1288 if ( !impl_hasHelpInstalled() )
1290 SvtHelpOptions aHelpOptions
;
1291 bool bShowOfflineHelpPopUp
= aHelpOptions
.IsOfflineHelpPopUp();
1293 TopLevelWindowLocker aBusy
;
1295 if(bShowOfflineHelpPopUp
)
1297 aBusy
.incBusy(pWidget
);
1298 HelpManualMessage
aQueryBox(pWidget
);
1299 short OnlineHelpBox
= aQueryBox
.run();
1300 bShowOfflineHelpPopUp
= OnlineHelpBox
!= RET_OK
;
1301 aHelpOptions
.SetOfflineHelpPopUp(aQueryBox
.GetOfflineHelpPopUp());
1304 if(!bShowOfflineHelpPopUp
)
1306 if ( impl_showOnlineHelp( aHelpURL
) )
1310 aBusy
.incBusy(pWidget
);
1311 NoHelpErrorBox
aErrBox(pWidget
);
1325 // old-help to display
1326 Reference
< XDesktop2
> xDesktop
= Desktop::create( ::comphelper::getProcessComponentContext() );
1328 // check if help window is still open
1329 // If not, create a new one and return access directly to the internal sub frame showing the help content
1330 // search must be done here; search one desktop level could return an arbitrary frame
1331 Reference
< XFrame2
> xHelp(
1332 xDesktop
->findFrame( "OFFICE_HELP_TASK", FrameSearchFlag::CHILDREN
),
1334 Reference
< XFrame
> xHelpContent
= xDesktop
->findFrame(
1336 FrameSearchFlag::CHILDREN
);
1338 SfxHelpWindow_Impl
* pHelpWindow
= nullptr;
1340 pHelpWindow
= impl_createHelp(xHelp
, xHelpContent
);
1342 pHelpWindow
= static_cast<SfxHelpWindow_Impl
*>(VCLUnoHelper::GetWindow(xHelp
->getComponentWindow()).get());
1343 if (!xHelp
.is() || !xHelpContent
.is() || !pHelpWindow
)
1346 SAL_INFO("sfx.appl", "HelpId = " << aHelpURL
);
1348 pHelpWindow
->SetHelpURL( aHelpURL
);
1349 pHelpWindow
->loadHelpContent(aHelpURL
);
1350 if (!rKeyword
.isEmpty())
1351 pHelpWindow
->OpenKeyword( rKeyword
);
1353 Reference
< css::awt::XTopWindow
> xTopWindow( xHelp
->getContainerWindow(), UNO_QUERY
);
1354 if ( xTopWindow
.is() )
1355 xTopWindow
->toFront();
1360 OUString
SfxHelp::CreateHelpURL(const OUString
& aCommandURL
, const OUString
& rModuleName
)
1362 SfxHelp
* pHelp
= static_cast< SfxHelp
* >(Application::GetHelp());
1363 return pHelp
? SfxHelp::CreateHelpURL_Impl( aCommandURL
, rModuleName
) : OUString();
1366 OUString
SfxHelp::GetDefaultHelpModule()
1368 return getDefaultModule_Impl();
1371 OUString
SfxHelp::GetCurrentModuleIdentifier()
1373 return getCurrentModuleIdentifier_Impl();
1376 bool SfxHelp::IsHelpInstalled()
1378 return impl_hasHelpInstalled();
1381 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */