Compare commits

...

14 Commits

Author SHA1 Message Date
Janek Bevendorff
f484d7f5ed Change Security/LockDatabaseIdle default to true 2025-11-16 17:22:00 +01:00
Janek Bevendorff
10bd651355 Enable CodeQL for all PRs and production branches 2025-11-16 09:51:48 +01:00
Janek Bevendorff
b3dbc49161 Remove theme-based menubar icon toggle on macOS
The menubar theme detection on macOS has always been wonky, and with Liquid Glass it has become entirely useless. This removes the icon theme switch and uses the monochrome light icon as a mask until we find a better solution. This should look okay in most cases, unless the user has a very bright wallpaper.
2025-11-15 20:14:15 +01:00
Janek Bevendorff
eefee1f092 Install macOS bundle icons on build
Installs bundle icons to the Resources folder during the build stage and not during the install stage. This ensures that the app has an icon when run directly from the .app folder inside the IDE. Previously, the icon would be installed only when running make install or cpack.
2025-11-15 00:57:30 +01:00
Janek Bevendorff
5332075193 Update sponsors list and translators fetch script 2025-11-12 16:24:22 +01:00
Jonathan White
964bb59f71 Fix error in hardware key detection code on Windows 2025-11-09 16:11:38 -05:00
Jonathan White
5acfcc6a1f Prevent interface lockups during startup with multiple tabs
Fixes #11998

Avoids UI lockups by removing several unnecessary mutex blocks  and avoiding redundant key detection calls.

Detect Yubikeys dynamically when challenging:

Prevents issue where correct key cannot be found if the internal state was reset prior to saving

This can occur if a user has multiple tabs open and multiple keys connected. Then switches to a locked tab without their DB key inserted which resets detection state.

Side Benefit - ensures proper cascade between USB and PC/SC interfaces so users can switch between the two modes seamlessly.
2025-11-09 16:11:38 -05:00
Jonathan White
d87554e6d2 Implement Group sync for KeeShare (#11593)
---------

Co-authored-by: ever <ever@brokenmouse.studio>
Co-authored-by: Ben Kluwe <ben.kl@go4more.de>
2025-11-09 16:10:45 -05:00
Jonathan White
e542070902 Allow for escape syntax to enable literal placeholders
* Fixes #11890
2025-11-09 10:45:07 -05:00
Jonathan White
301c64d68c Don't clear clipboard if previously cleared
* Fixes #12591
2025-11-09 10:44:33 -05:00
Janek Bevendorff
77746846da Add Liquid Glass icon 2025-11-09 15:53:11 +01:00
Sven Strickroth
002b8fee15 Do not show misleading error message if user clicked cancel
Signed-off-by: Sven Strickroth <email@cs-ware.de>
2025-11-09 09:25:00 -05:00
Sven Strickroth
44daca921a Escape accelerators
(fixes issue #12037)

Signed-off-by: Sven Strickroth <email@cs-ware.de>
2025-11-07 15:47:14 -05:00
Markus Theil
fedcbf60c5 fix build with Botan 3.10
This fixes a compiler error I got,
when trying to build with Botan 3.10.

A static_cast to RSA_PrivateKey was not possible,
as the base class is virtual.

Fix by using a dynamic_cast instead.

Signed-off-by: Markus Theil <theil.markus@gmail.com>
2025-11-07 15:46:34 -05:00
38 changed files with 10824 additions and 1272 deletions

View File

@@ -2,10 +2,10 @@ name: "CodeQL"
on:
push:
branches: [ 'develop', 'release/2.7.x' ]
branches:
- 'develop'
- 'release/**'
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'develop' ]
schedule:
- cron: '5 16 * * 3'

View File

@@ -49,6 +49,8 @@ This section contains full details on advanced features available in KeePassXC.
|{DB_DIR} |Absolute directory path of database file
|===
NOTE: You can insert literal placeholder strings by escaping the beginning and ending curly braces. For example, to insert the string `{USERNAME}`, you would type `++\{USERNAME\}++`.
=== Entry Cross-Reference
A reference to another entry's field is possible using the shorthand syntax:
`{REF:&lt;FIELD&gt;@&lt;SEARCH_IN&gt;:&lt;SEARCH_TEXT&gt;}`

View File

@@ -67,10 +67,6 @@ if(UNIX AND NOT APPLE AND NOT HAIKU)
install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
endif(UNIX AND NOT APPLE AND NOT HAIKU)
if(APPLE)
install(FILES macosx/keepassxc.icns DESTINATION ${DATA_INSTALL_DIR})
endif()
if(WIN32)
install(FILES windows/qt.conf DESTINATION ${BIN_INSTALL_DIR})
endif()
@@ -85,7 +81,16 @@ add_custom_command(TARGET icons
if(APPLE)
add_custom_command(TARGET icons
POST_BUILD
COMMAND png2icns macosx/keepassxc.icns icons/application/256x256/apps/keepassxc.png
COMMAND xcrun actool share/macosx/keepassxc.icon
--compile share/macosx
--output-partial-info-plist /dev/null
--app-icon keepassxc
--include-all-app-icons
--enable-on-demand-resources NO
--target-device mac
--minimum-deployment-target 11.0
--platform macosx
--output-format human-readable-text
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()

File diff suppressed because one or more lines are too long

BIN
share/macosx/Assets.car Normal file

Binary file not shown.

View File

@@ -13,9 +13,11 @@
<key>CFBundleExecutable</key>
<string>${PROGNAME}</string>
<key>CFBundleIconFile</key>
<string>keepassxc.icns</string>
<string>${MACOSX_BUNDLE_ICON_NAME}.icns</string>
<key>CFBundleIconName</key>
<string>${MACOSX_BUNDLE_ICON_NAME}</string>
<key>CFBundleIdentifier</key>
<string>org.keepassxc.keepassxc</string>
<string>${MACOSX_BUNDLE_IDENTIFIER}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

Binary file not shown.

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient-2);mix-blend-mode:lighten;opacity:.7;}.cls-3{fill:url(#radial-gradient);}.cls-4{isolation:isolate;}</style><radialGradient id="radial-gradient" cx="315.9556" cy="395.3416" fx="315.9556" fy="395.3416" r="239.1689" gradientTransform="translate(-46.999 -42.8948) scale(.3539 .2026)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#2e6b26"/><stop offset="1" stop-color="#6ab536"/></radialGradient><radialGradient id="radial-gradient-2" cx="314.1662" cy="394.0804" fx="314.1662" fy="394.0804" r="46.7089" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6ab536"/><stop offset="1" stop-color="#2e6b26"/></radialGradient></defs><g class="cls-4"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><path id="Background" class="cls-3" d="M63.9999,24.1601c-21.9679,0-39.84,17.8721-39.84,39.8399s17.8721,39.8399,39.84,39.8399,39.8399-17.8722,39.8399-39.8399-17.8719-39.8399-39.8399-39.8399Z"/><path id="Lighten_Hole" class="cls-2" d="M63.9998,27.4724c-6.5434,0-11.8668,5.3234-11.8668,11.8668s5.3234,11.8668,11.8668,11.8668,11.8668-5.3235,11.8668-11.8668-5.3234-11.8668-11.8668-11.8668Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient);opacity:.3;}.cls-2,.cls-3{mix-blend-mode:multiply;}.cls-4{fill:url(#radial-gradient-2);}.cls-5{isolation:isolate;}.cls-3{fill:#0f0f0d;opacity:.2;}</style><radialGradient id="radial-gradient" cx="322.2841" cy="405.7418" fx="322.2841" fy="405.7418" r="68.9894" gradientTransform="translate(-44.2199 -179.8556) scale(.3356 .5572)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000"/><stop offset=".7842" stop-color="#4f4f4f" stop-opacity="0"/></radialGradient><radialGradient id="radial-gradient-2" cx="313.1713" cy="380.0413" fx="313.1713" fy="380.0413" r="159.6501" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f5f5f5"/><stop offset=".5036" stop-color="#f2f2f2"/></radialGradient></defs><g class="cls-5"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><path id="Key_Double_Shadow" class="cls-2" d="M43.0622,29.9746c-.0019.0137-.0038.027-.0058.0406-.0518.0097-.0526-.0026.0058-.0406ZM73.1463,55.5462l-.551,9.8444,6.5017,7.0832-6.5017,7.0832,4.2977,4.6821-4.2977,4.6821.551,9.764h-18.2929v-43.139c-7.2731-3.7217-12.232-11.7653-12.232-21.0094,0-1.5471.2166-2.9765.435-4.5215.789-.1487,13.5481-5.4962,13.5481-5.4962-.1059.578,14.8965.578,14.7906,0,0,0,12.3269,5.3041,13.5637,5.5642.3128,1.5189.4195,2.9293.4195,4.4534,0,9.2442-5.0691,17.2878-12.232,21.0095ZM84.9375,29.9746c.0077.0365.0138.0722.0213.1086.1354.0285.14-.0025-.0213-.1086Z"/><path id="Key_Drop_Shadow" class="cls-3" d="M42.6543,90.0228c-7.0698-6.1486-11.5457-15.2121-11.5457-25.3548,0-11.8019,6.172-22.696,16.3377-28.7786-.1815,1.1802-.3631,2.2696-.3631,3.4498,0,6.9903,4.0844,13.0729,10.0749,15.8872v31.8652l7.5335,7.5351,7.5335-7.5351-.4538-6.6272,3.5398-3.5406-3.5398-3.5406,5.3551-5.3563-5.3551-5.3563.4538-7.4443c5.8997-2.8143,10.0749-8.8968,10.0749-15.8872,0-1.1802-.0908-2.2696-.3631-3.4498,3.6888,2.227,6.8516,5.1011,9.3871,8.4266-6.1315-8.4206-16.0924-13.8991-27.3404-13.8991-18.6365,0-33.7443,15.0357-33.7443,33.5832,0,10.495,4.8383,19.8644,12.4149,26.0227ZM62.241,84.5497h-2.5414v-25.1472h2.5414v25.1472ZM58.6104,31.7133c1.9968-.3631,4.0844-.5447,6.0813-.5447,2.0876-.0908,4.0844.1815,6.0813.5447.0908.4539.1815.9078.1815,1.3617,0,3.4498-2.8137,6.2641-6.2628,6.2641s-6.2628-2.8143-6.2628-6.2641c0-.4539.0908-.9078.1815-1.3617Z"/><path id="Key" class="cls-4" d="M63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399ZM57.9186,31.0088c1.9968-.3631,4.0844-.5447,6.0813-.5447,2.0876-.0908,4.0844.1815,6.0813.5447.0908.4539.1815.9078.1815,1.3617,0,3.4498-2.8137,6.2641-6.2628,6.2641s-6.2628-2.8143-6.2628-6.2641c0-.4539.0908-.9078.1815-1.3617ZM61.5492,58.6979v25.1472h-2.5414v-25.1472h2.5414ZM63.9999,97.5535c-18.5161,0-33.5831-14.9794-33.5831-33.5901,0-11.8019,6.172-22.696,16.3377-28.7786-.1815,1.1802-.3631,2.2696-.3631,3.4498,0,6.9903,4.0844,13.0729,10.0749,15.8872v31.8652l7.5335,7.5351,7.5335-7.5351-.4538-6.6272,3.5398-3.5406-3.5398-3.5406,5.3551-5.3563-5.3551-5.3563.4538-7.4443c5.8997-2.8143,10.0749-8.8968,10.0749-15.8872,0-1.1802-.0908-2.2696-.3631-3.4498,10.0749,6.0826,16.247,16.9766,16.3377,28.7786,0,18.5199-14.9763,33.5901-33.5831,33.5901Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 128 128"><defs><style>.cls-1{fill:none;}.cls-2{fill:url(#radial-gradient-2);opacity:.44;}.cls-3{fill:#0f0f0d;opacity:.35;}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#linear-gradient);}.cls-6{isolation:isolate;}.cls-7{fill:url(#radial-gradient);mix-blend-mode:lighten;opacity:.32;}.cls-8{opacity:.6;}.cls-9{fill:rgba(15,15,13,.35);}</style><linearGradient id="linear-gradient" x1="63.9998" y1="20.4513" x2="63.9998" y2="107.5488" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#000"/><stop offset="1" stop-color="#000" stop-opacity=".2"/></linearGradient><linearGradient id="linear-gradient-2" x1="63.9998" y1="15.9186" x2="63.9998" y2="102.9186" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#414141"/><stop offset=".196" stop-color="#3e3e3e"/><stop offset="1" stop-color="#3a3a3a"/></linearGradient><radialGradient id="radial-gradient" cx="305.8904" cy="449.6754" fx="305.8904" fy="449.6754" r="148.0242" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset=".6733" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-color="#fcfcfc"/></radialGradient><radialGradient id="radial-gradient-2" cx="311.8517" cy="469.1737" fx="311.8517" fy="469.1737" r="123.1352" gradientTransform="translate(-46.999 -102.0755) scale(.3539)" gradientUnits="userSpaceOnUse"><stop offset=".1088" stop-color="#0f0f0d" stop-opacity="0"/><stop offset=".8856" stop-color="#414141" stop-opacity="0"/><stop offset=".9339" stop-color="#1e1e1d" stop-opacity=".6841"/><stop offset=".9874" stop-color="#0f0f0d"/></radialGradient></defs><g class="cls-6"><g id="Layer_2"><g id="Icon_macOS"><rect class="cls-1" width="128" height="128"/><g id="Countour"><g class="cls-8"><path class="cls-5" d="M63.9999,20.8039c23.7037,0,42.9881,19.3777,42.9881,43.1962s-19.2844,43.1962-42.9881,43.1962-42.9882-19.3777-42.9882-43.1962S40.2961,20.8039,63.9999,20.8039M63.9999,20.4513c-23.8983,0-43.3408,19.5358-43.3408,43.5487s19.4425,43.5487,43.3408,43.5487,43.3406-19.536,43.3406-43.5487-19.4423-43.5487-43.3406-43.5487h0Z"/></g></g><path id="Rim" class="cls-4" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399Z"/><path id="Bottom_Light" class="cls-7" d="M63.9999,20.517c-23.9767,0-43.4831,19.5063-43.4831,43.483s19.5064,43.483,43.4831,43.483,43.4829-19.5065,43.4829-43.483-19.5062-43.483-43.4829-43.483Z"/><path id="Inner_Shadow" class="cls-2" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,24.1601c-21.9582,0-39.8225,17.8721-39.8225,39.8399s17.8643,39.8399,39.8225,39.8399,39.8223-17.8723,39.8223-39.8399-17.8641-39.8399-39.8223-39.8399Z"/><path id="Bottom_Drop_Shadow" class="cls-9" d="M63.9999,24.3931c-22.1274,0-40.1292,18.0018-40.1292,40.1291s18.0018,40.1291,40.1292,40.1291,40.129-18.002,40.129-40.1291-18.0017-40.1291-40.129-40.1291Z"/><path id="Top_Drop_Shadow" class="cls-3" d="M63.9999,107.5c-23.9861,0-43.5001-19.5141-43.5001-43.5S40.0138,20.5,63.9999,20.5s43.4999,19.5139,43.4999,43.5-19.5138,43.5-43.4999,43.5ZM63.9999,21.431c-23.986,0-43.5001,19.2544-43.5001,42.9213s19.5141,42.9213,43.5001,42.9213,43.4999-19.2546,43.4999-42.9213-19.5138-42.9213-43.4999-42.9213Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -0,0 +1,99 @@
{
"fill-specializations" : [
{
"value" : "automatic"
},
{
"appearance" : "dark",
"value" : "automatic"
}
],
"groups" : [
{
"blur-material" : null,
"layers" : [
{
"glass" : true,
"image-name" : "macos-comp-key.svg",
"name" : "key",
"position" : {
"scale" : 9.5,
"translation-in-points" : [
0,
0
]
}
}
],
"lighting" : "individual",
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.3
}
},
{
"blur-material" : null,
"layers" : [
{
"glass" : true,
"image-name" : "macos-comp-bg-green.svg",
"name" : "bg-green",
"position" : {
"scale" : 9.5,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
},
{
"blur-material" : 0.5,
"hidden" : false,
"layers" : [
{
"glass" : true,
"image-name" : "macos-comp-rim.svg",
"name" : "rim",
"position" : {
"scale" : 9.5,
"translation-in-points" : [
0,
0
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.7
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.2
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

View File

@@ -3735,6 +3735,14 @@ Supported extensions are: %1.</source>
<source>Select import/export file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Maintain group structure with shared database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Keep Group Structure</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>EditGroupWidgetMain</name>
@@ -10590,11 +10598,7 @@ Example: JBSWY3DPEHPK3PXP</source>
<context>
<name>YubiKey</name>
<message>
<source>General: </source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not find interface for hardware key with serial number %1. Please connect it to continue.</source>
<source>Could not find hardware key with serial number %1. Please connect it to continue.</source>
<translation type="unfinished"></translation>
</message>
</context>
@@ -10655,10 +10659,6 @@ Example: JBSWY3DPEHPK3PXP</source>
</context>
<context>
<name>YubiKeyInterfacePCSC</name>
<message>
<source>Could not find or access hardware key with serial number %1. Please present it to continue. </source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Hardware key is locked or timed out. Unlock or re-present it to continue.</source>
<translation type="unfinished"></translation>

View File

@@ -421,7 +421,12 @@ if(WIN32)
endif()
# Main Executable Definition
add_executable(${PROGNAME} main.cpp)
target_link_libraries(${PROGNAME} keepassxc_gui)
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
if(WIN32)
set_target_properties(${PROGNAME} PROPERTIES WIN32 ON)
include(GenerateProductVersion)
generate_product_version(
WIN32_ResourceFiles
@@ -432,20 +437,24 @@ if(WIN32)
VERSION_PATCH ${KEEPASSXC_VERSION_PATCH}
)
list(APPEND WIN32_ResourceFiles "${CMAKE_SOURCE_DIR}/share/windows/icon.rc")
endif()
target_sources(${PROGNAME} PUBLIC ${WIN32_ResourceFiles})
add_executable(${PROGNAME} WIN32 main.cpp ${WIN32_ResourceFiles})
target_link_libraries(${PROGNAME} keepassxc_gui)
set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
# macOS App Bundle
if(APPLE AND WITH_APP_BUNDLE)
install(FILES ${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile DESTINATION ${BUNDLE_INSTALL_DIR})
configure_file(${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
elseif(APPLE AND WITH_APP_BUNDLE)
set(MACOSX_BUNDLE_IDENTIFIER org.keepassxc.keepassxc)
set(MACOSX_BUNDLE_ICON_NAME keepassxc)
configure_file("${CMAKE_SOURCE_DIR}/share/macosx/Info.plist.cmake" ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
install(FILES "${CMAKE_SOURCE_DIR}/share/macosx/embedded.provisionprofile" DESTINATION ${BUNDLE_INSTALL_DIR})
set(MACOSX_BUNDLE_RESOURCE_FILES
"${CMAKE_SOURCE_DIR}/share/macosx/Assets.car"
"${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.icns"
)
set_target_properties(${PROGNAME} PROPERTIES
MACOSX_BUNDLE ON
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements")
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist"
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements"
RESOURCE "${MACOSX_BUNDLE_RESOURCE_FILES}"
)
target_sources(${PROGNAME} PUBLIC ${MACOSX_BUNDLE_RESOURCE_FILES})
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"

View File

@@ -139,7 +139,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::Security_ClearSearch, {QS("Security/ClearSearch"), Roaming, false}},
{Config::Security_ClearSearchTimeout, {QS("Security/ClearSearchTimeout"), Roaming, 5}},
{Config::Security_HideNotes, {QS("Security/Security_HideNotes"), Roaming, false}},
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, false}},
{Config::Security_LockDatabaseIdle, {QS("Security/LockDatabaseIdle"), Roaming, true}},
{Config::Security_LockDatabaseIdleSeconds, {QS("Security/LockDatabaseIdleSeconds"), Roaming, 240}},
{Config::Security_LockDatabaseMinimize, {QS("Security/LockDatabaseMinimize"), Roaming, false}},
{Config::Security_LockDatabaseScreenLock, {QS("Security/LockDatabaseScreenLock"), Roaming, true}},

View File

@@ -1138,6 +1138,15 @@ QString Entry::resolveMultiplePlaceholdersRecursive(const QString& str, int maxD
return str;
}
// Short circuit if we have escaped the placeholder brackets
if (str.startsWith("\\{") && str.endsWith("\\}")) {
// Replace the escaped brackets with actuals and move on
auto ret = str;
ret.replace(0, 2, "{");
ret.replace(ret.size() - 2, 2, "}");
return ret;
}
QString result;
auto matches = placeholderRegEx.globalMatch(str);
int capEnd = 0;

View File

@@ -449,6 +449,11 @@ namespace Tools
return userName;
}
QString escapeAccelerators(QString string)
{
return string.replace("&", "&&");
}
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
{
QVariantMap result;

View File

@@ -49,6 +49,7 @@ namespace Tools
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment());
QString cleanFilename(QString filename);
QString cleanUsername();
QString escapeAccelerators(QString string);
template <class T> QSet<T> asSet(const QList<T>& a)
{

View File

@@ -44,29 +44,43 @@ static const QString aboutContributors = R"(
<li>Sergey Vilgelm</li>
<li>Victor Engmark</li>
<li>NarwhalOfAges</li>
<li>No Name</li>
<li>SG</li>
<li>Riley Moses</li>
<li>Esteban Martinez</li>
<li>Marc Morocutti</li>
<li>Zivix</li>
<li>Benedikt Heine</li>
<li>Hugo Locurcio</li>
<li>William Petrides</li>
<li>Gunar Gessner</li>
<li>Christian Wittenhorst</li>
<li>Matt Cardarelli</li>
<li>Paul Ammann</li>
<li>Steve Isom</li>
<li>GodSpell</li>
<li>Lionel Laské</li>
<li>Daniel Epp</li>
<li>Oleksii Aleksieiev</li>
<li>Julian Stier</li>
<li>Ruben Schade</li>
<li>Bernhard</li>
<li>Wojciech Kozlowski</li>
<li>Caleb Currie</li>
<li>Morgan Courbet</li>
<li>Kyle Kneitinger</li>
<li>Chris Sohns</li>
<li>Shmavon Gazanchyan</li>
<li>xjdwc</li>
<li>Riley Moses</li>
<li>Igor Zinovik</li>
<li>Jeff</li>
<li>Esteban Martinez</li>
<li>Max Andersen</li>
<li>Zivix</li>
<li>Marc Morocutti</li>
<li>super scampy</li>
<li>Hugo Locurcio</li>
<li>Benedikt Heine</li>
<li>Mischa Peters</li>
<li>Rainer-Maria Fritsch</li>
<li>Micha Ober</li>
<li>Ivan Gromov</li>
<li>William Petrides</li>
<li>Joshua Go</li>
<li>Gunar Gessner</li>
<li>pancakeplant</li>
<li>Hans-Joachim Forker</li>
<li>Nicolas Vandemaele</li>
@@ -75,30 +89,66 @@ static const QString aboutContributors = R"(
<li>Mike</li>
<li>Thomas Renz</li>
<li>Toby Cline</li>
<li>Christian Wittenhorst</li>
<li>Paul Ammann</li>
<li>Matt Cardarelli</li>
<li>Steve Isom</li>
<li>Emre Dessoi</li>
<li>Wojciech Kozlowski</li>
<li>Michael Babnick</li>
<li>kernellinux</li>
<li>Patrick Evans</li>
<li>Marco</li>
<li>GodSpell</li>
<li>Jeremy Rubin</li>
<li>Korbi</li>
<li>andreas</li>
<li>Tyche's tidings</li>
<li>Daniel Kuebler</li>
<li>Brandon Corujo</li>
<li>AheroX</li>
<li>Alexandre G</li>
<li>AshinaGa</li>
<li>BYTEBOLT</li>
<li>CEH</li>
<li>Chris Stone</li>
<li>Christof Böckler</li>
<li>Claude</li>
<li>CzLer</li>
<li>Daniel Burridge</li>
<li>dark</li>
<li>Dave G</li>
<li>David Bowers</li>
<li>dickv</li>
<li>fp4</li>
<li>Huser IT Solutions GmbH</li>
<li>irf</li>
<li>Isaiah Rahmany</li>
<li>JackNYC</li>
<li>Jacob Emmert-Aronson</li>
<li>John Donadeo</li>
<li>Kosta Medinsky</li>
<li>leinad987</li>
<li>Lux</li>
<li>marek</li>
<li>mattlongname</li>
<li>mattock</li>
<li>Max Christian Pohle</li>
<li>nta/norma</li>
<li>picatsv</li>
<li>proto</li>
<li>Raymond Lau</li>
<li>Waido</li>
<li>Weinmann Willy</li>
<li>WildMage</li>
</ul>
<h3>VIP GitHub Sponsors:</h3>
<ul>
<li>mercedes-benz</li>
<li>tiangolo</li>
<li>mrniko</li>
<li>rszamszur</li>
</ul>
<h3>Notable Code Contributions:</h3>
<ul>
<li>droidmonkey</li>
<li>phoerious</li>
<li>louib (CLI)</li>
<li>varjolintu (Browser Integration)</li>
<li>louib (CLI)</li>
<li>hifi (SSH Agent)</li>
<li>xvallspl (Tags)</li>
<li>Aetf (FdoSecrets Storage Server)</li>
@@ -109,6 +159,8 @@ static const QString aboutContributors = R"(
<li>brainplot (many improvements)</li>
<li>kneitinger (many improvements)</li>
<li>frostasm (many improvements)</li>
<li>libklein (many improvements)</li>
<li>w15eacre (many improvements)</li>
<li>fonic (Entry Table View)</li>
<li>kylemanna (YubiKey)</li>
<li>c4rlo (Offline HIBP Checker)</li>
@@ -121,179 +173,145 @@ static const QString aboutContributors = R"(
</ul>
<h3>Patreon Supporters:</h3>
<ul>
<li>Richard Ames</li>
<li>Bernhard</li>
<li>Christian Rasmussen</li>
<li>Nuutti Toivola</li>
<li>Lionel Laské</li>
<li>Tyler Gass</li>
<li>NZSmartie</li>
<li>Darren</li>
<li>Brad</li>
<li>Oleksii Aleksieiev</li>
<li>Julian Stier</li>
<li>Daniel Epp</li>
<li>Ruben Schade</li>
<li>William Komanetsky</li>
<li>Niels Ganser</li>
<li>judd</li>
<li>Tarek Sherif</li>
<li>Eugene</li>
<li>CYB3RL4MBD4</li>
<li>Alexanderjb</li>
<li>Justin Carroll</li>
<li>Bart Libert</li>
<li>Shintaro Matsushima</li>
<li>Thammachart Chinvarapon</li>
<li>Gernot Premper</li>
<li>SLmanDR</li>
<li>Paul Ellenbogen</li>
<li>John C</li>
<li>Markus Wochnik</li>
<li>Clark Henry</li>
<li>zapscribe</li>
<li>Salt Rock Lamp</li>
<li>Steven Crowley</li>
<li>Ralph Azucena</li>
<li>Guruprasad Kulkarni</li>
<li>jose</li>
<li>Michael Gulick</li>
<li>J Doty</li>
<li>Synchro11</li>
<li>Michael Soares</li>
<li>Johannes Felko</li>
<li>Ellie</li>
<li>David Walluscheck</li>
<li>Anthony Avina</li>
<li>pro</li>
<li>Mark Luxton</li>
<li>Markus</li>
<li>Crimson Idol</li>
<li>Björn König</li>
<li>René Weselowski</li>
<li>gonczor</li>
<li>Steven</li>
<li>Ellie</li>
<li>Anthony Avina</li>
<li>PlushElderGod</li>
<li>gilgwath</li>
<li>Tobias</li>
<li>zapscribe</li>
<li>Christopher Hillenbrand</li>
<li>Daddy's c$sh</li>
<li>Ashura</li>
<li>Florian</li>
<li>Alexandre</li>
<li>Dave Jones</li>
<li>Brett</li>
<li>Jim Vanderbilt</li>
<li>Brian McGuire</li>
<li>Sid Beske</li>
<li>Dmitrii Galinskii</li>
<li>Johannes Erchen</li>
<li>Brandon Zhang</li>
<li>Maxley Fraser</li>
<li>Nikul Savasadia</li>
<li>Claude</li>
<li>alga</li>
<li>Philipp Jetschina</li>
<li>Ralph Azucena</li>
<li>Florian</li>
<li>Kristoffer Winther Balling</li>
<li>Peter Link</li>
<li>Vlastimil Vondra</li>
<li>Tony Wang</li>
<li>John Sivak</li>
<li>Nol Aders</li>
<li>Charlie Drake</li>
<li>Ryan Goldstein</li>
<li>Doug Witt</li>
<li>David S H Rosenthal</li>
<li>Lance Simmons</li>
<li>Mathew Woodyard</li>
<li>GanderPL</li>
<li>Neša</li>
<li>tolias</li>
<li>Michael Soares</li>
<li>Vlad Didenko</li>
<li>henloo</li>
<li>Erik Rigtorp</li>
<li>Barry McKenzie</li>
<li>Sebastian van der Est</li>
<li>J.C. Polanycia</li>
<li>Peter Upfold</li>
<li>Josh Yates-Walker</li>
<li>Adam</li>
<li>HJ</li>
<li>bjorndown</li>
<li>Tony Wang</li>
<li>Nol Aders</li>
<li>Dirk Bergstrom</li>
<li>proco</li>
<li>Philipp Baderschneider</li>
<li>Neša</li>
<li>Dimitris Kogias</li>
<li>Robin Hellsten</li>
<li>Scott Williams</li>
<li>klepto68</li>
<li>Uwe S.</li>
<li>codiflow</li>
<li>eugene</li>
<li>Anton Fisher</li>
<li>David Daly</li>
<li>Crispy_Steak</li>
<li>Cilestin</li>
<li>Benjamin</li>
<li>Daniel Lakeland</li>
<li>erinacio</li>
<li>Leo</li>
<li>Payton</li>
<li>Saicxs</li>
<li>Gorund O</li>
<li>Tony G</li>
<li>Simonas S.</li>
<li>LordKnoppers</li>
<li>Fabien Duchaussois</li>
<li>Tim Bahnes</li>
<li>Aleksei Gusev</li>
<li>J Hanssen</li>
<li>schoepp</li>
<li>Klaus</li>
<li>Eric</li>
<li>Griffondale</li>
<li>Andy D</li>
<li>YAMAMOTO Yuji</li>
<li>elmiko</li>
<li>David</li>
<li>Nate Wynd</li>
<li>Nicolas</li>
<li>magila</li>
<li>Bryan Fisher</li>
<li>Mark Nicholson</li>
<li>Asperatus</li>
<li>Patrick Buchan-Hepburn</li>
<li>Richárd Faragó</li>
<li>David Koch</li>
<li>cheese_cake</li>
<li>duke_money</li>
<li>lund</li>
<li>Ivana Kellyer</li>
<li>Skullzam</li>
<li>Chris Bier</li>
<li>Gustavo</li>
<li>Henning_IdB</li>
<li>Edd</li>
<li>Net</li>
<li>Sergei Slipchenko</li>
<li>Amanita</li>
<li>Gaara</li>
<li>Max</li>
<li>5h4d3</li>
<li>James Taylor</li>
<li>Alexei Bond</li>
<li>cck</li>
<li>David L</li>
<li>devNull</li>
<li>Erica</li>
<li>Matthew O</li>
<li>Druggo Yang</li>
<li>Eric Stokes</li>
</ul>
<h3>GitHub Sponsors:</h3>
<ul>
<li>rszamszur</li>
<li>Sidicas</li>
<li>Mr-NH</li>
</ul>
<h3>Translations:</h3>
<ul>
<li><strong>العربية (Arabic)</strong>: 3eani, 3nad, AboShanab, butterflyoffire_temp, jBailony, kmutahar, m.hemoudi,
Marouane87, microtaha, mohame1d, muha_abdulaziz, Night1, omar.nsy, TheAhmed, zer0x</li>
<li><strong>euskara (Basque)</strong>: azken_tximinoa, Galaipa, Hey_neken, porrumentzio</li>
<li><strong> (Bengali)</strong>: codesmite, Foxom, rediancool, RHJihan</li>
<li><strong> (Burmese)</strong>: Christine.Ivy, hafe14, Snooooowwwwwman, tuntunaung</li>
<li><strong>català (Catalan)</strong>: antoniopolonio, benLabcat, capitantrueno, dsoms, Ecron, jamalinu, jmaribau,
MarcRiera, mcus, raulua, zeehio, ZJaume</li>
<li><strong> (Chinese (Simplified))</strong>: Biacke, Biggulu, Brandon_c, carp0129, Clafiok, deluxghost, Dy64,
ef6, Felix2yu, hoilc, jy06308127, kikyous, kofzhanganguo, ligyxy, lxx4380, oksjd, remonli, ShuiHuo, sjdhome,
slgray, Small_Ku, snhun, umi_neko, vc5, Wylmer_Wang, Z4HD</li>
<li><strong> () (Chinese (Traditional))</strong>: BestSteve, Biacke, flachesis, gojpdchx, ligyxy, MiauLightouch,
plesry, priv, raymondtau, Siriusmart, Small_Ku, ssuhung, Superbil, th3lusive, yan12125, ymhuang0808</li>
<li><strong>hrvatski jezik (Croatian)</strong>: krekrekre, mladenuzelac</li>
<li><strong>čeština (Czech)</strong>: DanielMilde, jiri.jagos, pavelb, pavelz, S474N, stps, tpavelek, vojtechjurcik</li>
<li><strong>dansk (Danish)</strong>: alfabetacain, dovecode, ebbe, ERYpTION, GimliDk, Grooty12, JakobPP, KalleDK,
MannVera, nlkl, Saustrup, thniels</li>
<li><strong>Nederlands (Dutch)</strong>: apie, bartlibert, Bubbel, bython, Dr.Default, e2jk, evanoosten, fourwood,
fvw, glotzbach, JCKalman, keunes, KnooL, ms.vd.linden, ovisicnarf, pietermj, pvdl, rigrig, srgvg, Stephan_P,
stijndubrul, theniels17, ThomasChurchman, timschreinemachers, Vistaus, wanderingidea, Zombaya1</li>
<li><strong>Esperanto (Esperanto)</strong>: batisteo</li>
<li><strong>eesti (Estonian)</strong>: Hermanio, okul, sarnane, tlend, V6lur</li>
<li><strong>suomi (Finnish)</strong>: artnay, hif1, MawKKe, petri, tomisalmi, uusijani, varjolintu</li>
<li><strong>français (French)</strong>: ayiniho, Beatussum, butterflyoffire_temp, Cabirto, francoisa, iannick,
jean_pierre_raumer, John.Mickael, Jojo6375, lacnic, Marouane87, mohame1d, orion78fr, stephanecodes, swarmpan,
t0mmy742, Takeçi, tenzap, webafrancois, x0rld</li>
<li><strong>Galego (Galician)</strong>: damufo, enfeitizador, mustek</li>
<li><strong>Deutsch (German)</strong>: andreas.maier, antsas, archer_321, ASDFGamer, Atalanttore, BasicBaer, blacksn0w,
bwolkchen, Calyrx, clonejo, codejunky, DavidHamburg, eth0, fahstat, FlotterCodername, for1real, frlan, funny0facer,
Gyges, h_h, Hativ, heynemax, hjonas, HoferJulian, hueku, janis91, jensrutschmann, jhit, joe776, kflesch, man_at_home,
marcbone, MarcEdinger, markusd112, Marouane87, maxwxyz, mcliquid, mfernau77, mircsicz, montilo, MuehlburgPhoenix,
muellerma, nautilusx, neon64, Nerzahd, Nightwriter, noodles101, NotAName, nursoda, OLLI_S, omnisome4, origin_de,
pcrcoding, PFischbeck, phallobst, philje, pqtjhhBzDd5NuJ9, r3drock, rakekniven, revoltek, rgloor, Rheggie, RogueThorn,
rugk, ScholliYT, scotwee, Silas_229, spacemanspiff, SteffoSpieler, testarossa47, TheForcer, thillux, transi_222, traschke,
Unkn0wnCat, vlenzer, vpav, waster, wolfram.roesler, Wyrrrd, xf</li>
<li><strong>ελληνικά (Greek)</strong>: anvo, arttor, Dkafetzis, giwrgosmant, GorianM, Jason_M, magkopian, nplatis, saglogog,
tassos.b, xinomilo</li>
<li><strong>עברית (Hebrew)</strong>: avimar, ronyala, shemeshg, shmag18, ThunderB0lt, tryandtry, ztwersky</li>
<li><strong>magyar (Hungarian)</strong>: andras_tim, bubu, entaevau, kempelen, meskobalazs, spammy, typingseashell, urbalazs</li>
<li><strong>Íslenska (Icelandic)</strong>: MannVera</li>
<li><strong>Bahasa Indonesia (Indonesian)</strong>: achmad, algustionesa, bora_ach, racrbmr, zk</li>
<li><strong>Italiano (Italian)</strong>: aleb2000, amaxis, bovirus, duncanmid, FranzMari, Gringoarg, idetao, lucaim, NITAL, Peo,
Pietrog, salvatorecordiano, seatedscribe, Stemby, the.sailor, tosky, VosaxAlo</li>
<li><strong> (Japanese)</strong>: AlCooo, gojpdchx, helloguys, masoo, p2635, Shinichirou_Yamada, shortarrow, ssuhung, tadasu,
take100yen, Umoxfo, vargas.peniel, vmemjp, WatanabeShint, yukinakato</li>
<li><strong>қазақ тілі (Kazakh)</strong>: sotrud_nik</li>
<li><strong> (Korean)</strong>: BraINstinct0, cancantun, peremen</li>
<li><strong>latine (Latin)</strong>: alexandercrice</li>
<li><strong>latviešu valoda (Latvian)</strong>: andis.luksho, victormeirans, wakeeshi</li>
<li><strong>lietuvių kalba (Lithuanian)</strong>: Kornelijus, Moo, pauliusbaulius, rookwood101, wakeeshi</li>
<li><strong>Norsk Bokmål (Norwegian Bokmål)</strong>: bkvamme, eirikl, eothred, haarek, JardarBolin, jumpingmushroom, sattor,
torgeirf, ysteinalver</li>
<li><strong> (Punjabi)</strong>: aalam</li>
<li><strong>فارسی (Farsi)</strong>: gnulover, siamax</li>
<li><strong>فارسی (Farsi (Iran))</strong>: magnifico</li>
<li><strong>język polski (Polish)</strong>: AreYouLoco, dedal123, EsEnZeT, hoek, keypress, konradmb, mrerexx, pabli, ply,
psobczak, SebJez, verahawk</li>
<li><strong>Português (Portuguese)</strong>: diraol, hugok, pfialho, rudahximenes, weslly, xendez</li>
<li><strong>Português (Portuguese (Brazil))</strong>: alinda, amalvarenga, andersoniop, danielbibit, diraol, fabiom, flaviobn,
fmilagres, furious_, gabrieljcs, Guilherme.Peev, guilherme__sr, Havokdan, igorruckert, josephelias94, keeBR, kiskadee, lecalam,
lucasjsoliveira, mauri.andres, newmanisaac, rafaelnp, ruanmed, rudahximenes, ul1sses, vitor895, weslly, wtuemura, xendez,
zodSilence</li>
<li><strong>Português (Portuguese (Portugal))</strong>: a.santos, American_Jesus, arainho, hds, hugok, lecalam, lmagomes, pfialho,
smarquespt, smiguel, xendez, xnenjm</li>
<li><strong>Română (Romanian)</strong>: _parasite_, aduzsardi, alexminza, polearnik</li>
<li><strong>русский (Russian)</strong>: 3nad, _nomoretears_, agag11507, alexandersokol, alexminza, anm, artemkonenko, ashed,
BANOnotIT, burningalchemist, cl0ne, cnide, denoos, DG, DmitriyMaksimov, egorrabota, injseon, Japet, JayDi85, KekcuHa, kerastinell,
laborxcom, leo9uinuo98, Mogost, Mr.GreyWolf, MustangDSG, netforhack, NetWormKido, nibir, Olesya_Gerasimenko, onix, Orianti,
RKuchma, ruslan.denisenko, ShareDVI, Shevchuk, solodyagin, talvind, treylav, upupa, VictorR2007, vsvyatski, wakeeshi, Walter.S,
wkill95, wtigga, zOrg1331</li>
<li><strong>српски језик (Serbian)</strong>: ArtBIT, ozzii</li>
<li><strong>Slovenčina (Slovak)</strong>: Asprotes, crazko, jose1711, l.martinicky, pecer, reisuya, Slavko</li>
<li><strong>Slovenščina (Slovenian)</strong>: asasdasd, samodekleva</li>
<li><strong>Español (Spanish)</strong>: adolfogc, antifaz, capitantrueno, cquike, cyphra, DarkHolme, doubleshuffle, e2jk,
EdwardNavarro, fserrador, gabeweb, gonrial, jjtp, jorpilo, LeoBeltran, mauri.andres, piegope, pquin, puchrojo, rodolfo.guagnini,
tierracomun, vsvyatski</li>
<li><strong>Svenska (Swedish)</strong>: 0x9fff00, aiix, Anders_Bergqvist, ArmanB, Autom, baxtex, eson, henziger, jpyllman, malkus,
merikan, peron, peterkz, Thelin, theschitz, victorhggqvst</li>
<li><strong> (Thai)</strong>: arthit, ben_cm, chumaporn.t, darika, digitalthailandproject, GitJirasamatakij, ll3an, minoplhy,
muhammadmumean, nimid, nipattra, ordinaryjane, rayg, sirawat, Socialister, Wipanee</li>
<li><strong>Türkçe (Turkish)</strong>: abcmen, ahmed.ulusoy, cagries, denizoglu, desc4rtes, etc, ethem578, kayazeren, mcveri, N3pp,
rgucluer, SeLeNLeR, sprlptr48, TeknoMobil, Ven_Zallow, veysiertekin</li>
<li><strong>Українська (Ukrainian)</strong>: brisk022, chulivska, cl0ne, exlevan, m0stik, moudrick, netforhack, olko, onix, paul_sm,
ShareDVI, upupa, zoresvit</li>
<li><strong>Arabic:</strong> kmutahar</li>
<li><strong>Chinese (China):</strong> Biggulu, Brandon_c, hoilc, ligyxy, Small_Ku, umi_neko, vc5</li>
<li><strong>Chinese (Taiwan):</strong> BestSteve, flachesis, MiauLightouch, Small_Ku, yan12125, ymhuang0808</li>
<li><strong>Czech:</strong> DanielMilde, pavelb, tpavelek</li>
<li><strong>English (United Kingdom):</strong> YCMHARHZ</li>
<li><strong>English (United States):</strong> alexandercrice, DarkHolme, nguyenlekhtn</li>
<li><strong>Finnish:</strong> artnay, hif1, MawKKe, varjolintu</li>
<li><strong>German:</strong> antsas, BasicBaer, Calyrx, codejunky, DavidHamburg, eth0, for1real, jensrutschmann,
joe776, kflesch, marcbone, MarcEdinger, mcliquid, mfernau77, montilo, nursoda, omnisome4, origin_de, pcrcoding,
rgloor, vlenzer, waster, Wyrrrd</li>
<li><strong>Greek:</strong> magkopian, nplatis, tassos.b, xinomilo</li>
<li><strong>Hungarian:</strong> bubu, meskobalazs, urbalazs</li>
<li><strong>Indonesian:</strong> zk</li>
<li><strong>Italian:</strong> amaxis, duncanmid, FranzMari, lucaim, NITAL, Peo, tosky, VosaxAlo</li>
<li><strong>Japanese:</strong> masoo, p2635, Shinichirou_Yamada, vmemjp, yukinakato</li>
<li><strong>Korean:</strong> cancantun, peremen</li>
<li><strong>Lithuanian:</strong> Moo</li>
<li><strong>Norwegian Bokmål:</strong> eothred, haarek, torgeirf</li>
<li><strong>Polish:</strong> keypress, mrerexx, psobczak</li>
<li><strong>Portuguese (Brazil):</strong> danielbibit, fabiom, flaviobn, newmanisaac, vitor895, weslly</li>
<li><strong>Portuguese (Portugal):</strong> a.santos, American_Jesus, hds, lmagomes, smarquespt</li>
<li><strong>Romanian:</strong> alexminza</li>
<li><strong>Russian:</strong> _nomoretears_, agag11507, alexminza, anm, artemkonenko, denoos, KekcuHa, Mogost,
netforhack, NetWormKido, RKuchma, ShareDVI, talvind, VictorR2007, vsvyatski, wkill95</li>
<li><strong>Serbian:</strong> ArtBIT</li>
<li><strong>Swedish:</strong> Anders_Bergqvist, henziger, jpyllman, peron, Thelin</li>
<li><strong>Turkish:</strong> etc, N3pp</li>
<li><strong>Ukrainian:</strong> brisk022, netforhack, ShareDVI, zoresvit</li>
</ul>
)";

View File

@@ -100,8 +100,9 @@ void Clipboard::clearCopiedText()
return;
}
if (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|| m_lastCopied == clipboard->text(QClipboard::Selection)) {
if (!m_lastCopied.isEmpty()
&& (m_lastCopied == clipboard->text(QClipboard::Clipboard)
|| m_lastCopied == clipboard->text(QClipboard::Selection))) {
clipboard->clear(QClipboard::Clipboard);
clipboard->clear(QClipboard::Selection);
#ifdef Q_OS_UNIX

View File

@@ -674,7 +674,7 @@ void DatabaseTabWidget::updateTabName(int index)
return;
}
index = indexOf(dbWidget);
setTabText(index, tabName(index));
setTabText(index, Tools::escapeAccelerators(tabName(index)));
setTabToolTip(index, dbWidget->displayFilePath());
auto iconIndex = dbWidget->database()->publicIcon();
if (iconIndex >= 0 && iconIndex < databaseIcons()->count()) {

View File

@@ -92,12 +92,14 @@ QIcon Icons::trayIcon(bool unlocked)
}
QIcon i;
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
#if defined(Q_OS_WIN)
if (osUtils->isStatusBarDark()) {
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
} else {
i = icon(QString("keepassxc-monochrome-dark%1").arg(suffix), false);
}
#elif defined(Q_OS_MACOS)
i = icon(QString("keepassxc-monochrome-light%1").arg(suffix), false);
#else
i = icon(QString("%1-%2%3").arg(applicationIconName(), iconAppearance, suffix), false);
#endif

View File

@@ -38,6 +38,7 @@
#include "autotype/AutoType.h"
#include "core/InactivityTimer.h"
#include "core/Resources.h"
#include "core/Tools.h"
#include "gui/AboutDialog.h"
#include "gui/ActionCollection.h"
#include "gui/Icons.h"
@@ -789,7 +790,7 @@ void MainWindow::updateLastDatabasesMenu()
const QStringList lastDatabases = config()->get(Config::LastDatabases).toStringList();
for (const QString& database : lastDatabases) {
QAction* action = m_ui->menuRecentDatabases->addAction(database);
QAction* action = m_ui->menuRecentDatabases->addAction(Tools::escapeAccelerators(database));
action->setData(database);
m_lastDatabasesActions->addAction(action);
}

View File

@@ -132,6 +132,9 @@ void KeyFileEditWidget::browseKeyFile()
QString filters = QString("%1 (*.keyx *.key);;%2 (*)").arg(tr("Key files"), tr("All files"));
QString fileName = fileDialog()->getOpenFileName(this, tr("Select a key file"), QString(), filters);
if (fileName.isEmpty()) { // user clicked on cancel
return;
}
if (QFileInfo(fileName).canonicalFilePath() == m_parent->getDatabase()->canonicalFilePath()) {
MessageBox::critical(getMainWindow(),
tr("Invalid Key File"),

View File

@@ -56,7 +56,6 @@ void DeviceListenerWin::registerHotplugCallback(bool arrived,
regex += QString("PID_%1&").arg(productId, 0, 16).toUpper();
}
}
regex += QString(".*$"); // Qt won't match otherwise
m_deviceIdMatch = QRegularExpression(regex);
DEV_BROADCAST_DEVICEINTERFACE_W notificationFilter{
@@ -95,7 +94,7 @@ bool DeviceListenerWin::nativeEventFilter(const QByteArray& eventType, void* mes
|| (m_handleRemoval && m->wParam == DBT_DEVICEREMOVECOMPLETE)) {
const auto pBrHdr = reinterpret_cast<PDEV_BROADCAST_HDR>(m->lParam);
const auto pDevIface = reinterpret_cast<PDEV_BROADCAST_DEVICEINTERFACE_W>(pBrHdr);
const auto name = QString::fromWCharArray(pDevIface->dbcc_name, pDevIface->dbcc_size);
const auto name = QString::fromWCharArray(pDevIface->dbcc_name);
if (m_deviceIdMatch.match(name).hasMatch()) {
emit devicePlugged(m->wParam == DBT_DEVICEARRIVAL, nullptr, pDevIface);
return true;

View File

@@ -213,7 +213,7 @@ namespace KeeShareSettings
}
}
} else {
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -253,7 +253,7 @@ namespace KeeShareSettings
} else if (reader.name() == "PublicKey") {
own.certificate = Certificate::deserialize(reader);
} else {
qWarning("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
qDebug("Unknown KeeShareSettings element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -262,8 +262,7 @@ namespace KeeShareSettings
}
Reference::Reference()
: type(Inactive)
, uuid(QUuid::createUuid())
: uuid(QUuid::createUuid())
{
}
@@ -320,12 +319,21 @@ namespace KeeShareSettings
writer.writeStartElement("Password");
writer.writeCharacters(reference.password.toUtf8().toBase64());
writer.writeEndElement();
writer.writeStartElement("KeepGroups");
writer.writeCharacters(reference.keepGroups ? "True" : "False");
writer.writeEndElement();
});
}
Reference Reference::deserialize(const QString& raw)
{
if (raw.isEmpty()) {
return {};
}
Reference reference;
// If KeepGroups is not present, default to false for backward compatibility
reference.keepGroups = false;
xmlDeserialize(raw, [&](QXmlStreamReader& reader) {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "Type") {
@@ -346,8 +354,10 @@ namespace KeeShareSettings
reference.path = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else if (reader.name() == "Password") {
reference.password = QString::fromUtf8(QByteArray::fromBase64(reader.readElementText().toLatin1()));
} else if (reader.name() == "KeepGroups") {
reference.keepGroups = reader.readElementText().compare("True") == 0;
} else {
qWarning("Unknown Reference element %s", qPrintable(reader.name().toString()));
qDebug("Unknown Reference element %s", qPrintable(reader.name().toString()));
reader.skipCurrentElement();
}
}
@@ -363,7 +373,11 @@ namespace KeeShareSettings
// Extract RSA key data to serialize an ssh-rsa public key.
// ssh-rsa keys are currently not built into Botan
const auto rsaKey = static_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
// need a dynamic_cast here, because the base class is virtual
const auto rsaKey = dynamic_cast<Botan::RSA_PrivateKey*>(sign.certificate.key.data());
if (!rsaKey) {
return {};
}
std::vector<uint8_t> rsaE(rsaKey->get_e().bytes());
rsaKey->get_e().binary_encode(rsaE.data());

View File

@@ -122,10 +122,11 @@ namespace KeeShareSettings
struct Reference
{
Type type;
Type type = Inactive;
QUuid uuid;
QString path;
QString password;
bool keepGroups = true;
Reference();
bool isNull() const;

View File

@@ -62,6 +62,39 @@ namespace
}
}
void cloneIcon(Metadata* targetMetadata, const Database* sourceDb, const QUuid& iconUuid)
{
if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) {
targetMetadata->addCustomIcon(iconUuid, sourceDb->metadata()->customIcon(iconUuid));
}
}
void cloneEntries(Metadata* targetMetadata, const Group* sourceGroup, Group* targetGroup)
{
for (const Entry* sourceEntry : sourceGroup->entries()) {
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->setGroup(targetGroup);
targetEntry->setUpdateTimeinfo(updateTimeinfoEntry);
cloneIcon(targetMetadata, sourceEntry->database(), targetEntry->iconUuid());
}
}
void cloneChildren(Metadata* targetMetadata, const Group* sourceRoot, Group* targetRoot)
{
for (const Group* sourceGroup : sourceRoot->children()) {
auto* targetGroup = sourceGroup->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
const bool updateTimeinfo = targetGroup->canUpdateTimeinfo();
targetGroup->setUpdateTimeinfo(false);
targetGroup->setParent(targetRoot);
targetGroup->setUpdateTimeinfo(updateTimeinfo);
cloneIcon(targetMetadata, sourceRoot->database(), targetGroup->iconUuid());
cloneEntries(targetMetadata, sourceGroup, targetGroup);
cloneChildren(targetMetadata, sourceGroup, targetGroup);
}
}
Database* extractIntoDatabase(const KeeShareSettings::Reference& reference, const Group* sourceRoot)
{
const auto* sourceDb = sourceRoot->database();
@@ -75,17 +108,10 @@ namespace
targetRoot->setUpdateTimeinfo(false);
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
targetRoot->setUpdateTimeinfo(updateTimeinfo);
const auto sourceEntries = sourceRoot->entriesRecursive(false);
for (const Entry* sourceEntry : sourceEntries) {
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
const bool updateTimeinfoEntry = targetEntry->canUpdateTimeinfo();
targetEntry->setUpdateTimeinfo(false);
targetEntry->setGroup(targetRoot);
targetEntry->setUpdateTimeinfo(updateTimeinfoEntry);
const auto iconUuid = targetEntry->iconUuid();
if (!iconUuid.isNull() && !targetMetadata->hasCustomIcon(iconUuid)) {
targetMetadata->addCustomIcon(iconUuid, sourceEntry->database()->metadata()->customIcon(iconUuid));
}
cloneIcon(targetMetadata, sourceRoot->database(), targetRoot->iconUuid());
cloneEntries(targetMetadata, sourceRoot, targetRoot);
if (reference.keepGroups) {
cloneChildren(targetMetadata, sourceRoot, targetRoot);
}
auto key = QSharedPointer<CompositeKey>::create();

View File

@@ -43,6 +43,7 @@ EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
connect(m_ui->pathEdit, SIGNAL(editingFinished()), SLOT(selectPath()));
connect(m_ui->pathSelectionButton, SIGNAL(pressed()), SLOT(launchPathSelectionDialog()));
connect(m_ui->typeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(selectType()));
connect(m_ui->keepGroupsCheckbox, SIGNAL(toggled(bool)), SLOT(keepGroupsToggled(bool)));
connect(m_ui->clearButton, SIGNAL(clicked(bool)), SLOT(clearInputs()));
connect(KeeShare::instance(), SIGNAL(activeChanged()), SLOT(updateSharingState()));
@@ -97,6 +98,7 @@ void EditGroupWidgetKeeShare::updateSharingState()
m_ui->pathEdit->setEnabled(isEnabled);
m_ui->pathSelectionButton->setEnabled(isEnabled);
m_ui->passwordEdit->setEnabled(isEnabled);
m_ui->keepGroupsCheckbox->setEnabled(isEnabled);
if (!m_temporaryGroup || !isEnabled) {
m_ui->messageWidget->hideMessage();
@@ -188,6 +190,7 @@ void EditGroupWidgetKeeShare::update()
m_ui->typeComboBox->setCurrentIndex(reference.type);
m_ui->passwordEdit->setText(reference.password);
m_ui->pathEdit->setText(reference.path);
m_ui->keepGroupsCheckbox->setChecked(reference.keepGroups);
}
updateSharingState();
@@ -291,3 +294,13 @@ void EditGroupWidgetKeeShare::selectType()
updateSharingState();
}
void EditGroupWidgetKeeShare::keepGroupsToggled(bool toggled)
{
if (!m_temporaryGroup) {
return;
}
auto reference = KeeShare::referenceOf(m_temporaryGroup);
reference.keepGroups = toggled;
KeeShare::setReferenceTo(m_temporaryGroup, reference);
}

View File

@@ -48,6 +48,7 @@ private slots:
void selectPassword();
void launchPathSelectionDialog();
void selectPath();
void keepGroupsToggled(bool);
private:
QScopedPointer<Ui::EditGroupWidgetKeeShare> m_ui;

View File

@@ -138,7 +138,7 @@
</item>
</layout>
</item>
<item row="3" column="1">
<item row="4" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
@@ -171,7 +171,7 @@
</item>
</layout>
</item>
<item row="4" column="0">
<item row="5" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -184,6 +184,19 @@
</property>
</spacer>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="keepGroupsCheckbox">
<property name="toolTip">
<string>Maintain group structure with shared database</string>
</property>
<property name="text">
<string>Keep Group Structure</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@@ -73,31 +73,29 @@ bool YubiKey::isInitialized()
bool YubiKey::findValidKeys()
{
// Block operations on hardware keys while scanning
QMutexLocker lock(&s_interfaceMutex);
findValidKeys(lock);
m_connectedKeys = 0;
m_findingKeys = true;
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
m_findingKeys = false;
return !m_usbKeys.isEmpty() || !m_pcscKeys.isEmpty();
}
void YubiKey::findValidKeys(const QMutexLocker& locker)
{
// Check QMutexLocker since version 6.4
Q_UNUSED(locker);
m_connectedKeys = 0;
m_usbKeys = YubiKeyInterfaceUSB::instance()->findValidKeys(m_connectedKeys);
m_pcscKeys = YubiKeyInterfacePCSC::instance()->findValidKeys(m_connectedKeys);
}
void YubiKey::findValidKeysAsync()
{
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
// Don't start another scan if we are already doing one
if (!m_findingKeys) {
m_findingKeys = true;
QtConcurrent::run([this] { emit detectComplete(findValidKeys()); });
}
}
YubiKey::KeyMap YubiKey::foundKeys()
{
QMutexLocker lock(&s_interfaceMutex);
KeyMap foundKeys = m_usbKeys;
foundKeys.unite(m_pcscKeys);
@@ -106,38 +104,12 @@ YubiKey::KeyMap YubiKey::foundKeys()
int YubiKey::connectedKeys()
{
QMutexLocker lock(&s_interfaceMutex);
return m_connectedKeys;
}
QString YubiKey::errorMessage()
{
QMutexLocker lock(&s_interfaceMutex);
QString error;
error.clear();
if (!m_error.isNull()) {
error += tr("General: ") + m_error;
}
QString usb_error = YubiKeyInterfaceUSB::instance()->errorMessage();
if (!usb_error.isNull()) {
if (!error.isNull()) {
error += " | ";
}
error += "USB: " + usb_error;
}
QString pcsc_error = YubiKeyInterfacePCSC::instance()->errorMessage();
if (!pcsc_error.isNull()) {
if (!error.isNull()) {
error += " | ";
}
error += "PCSC: " + pcsc_error;
}
return error;
return m_error;
}
/**
@@ -175,25 +147,31 @@ bool YubiKey::testChallenge(YubiKeySlot slot, bool* wouldBlock)
YubiKey::ChallengeResult
YubiKey::challenge(YubiKeySlot slot, const QByteArray& challenge, Botan::secure_vector<char>& response)
{
QMutexLocker lock(&s_interfaceMutex);
m_error.clear();
// Make sure we tried to find available keys
if (m_usbKeys.isEmpty() && m_pcscKeys.isEmpty()) {
findValidKeys(lock);
// Prevent re-entrant access to hardware keys
QMutexLocker lock(&s_interfaceMutex);
// Try finding key on the USB interface first
auto ret = YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
if (ret == ChallengeResult::YCR_ERROR) {
m_error = YubiKeyInterfaceUSB::instance()->errorMessage();
return ret;
}
if (m_usbKeys.contains(slot)) {
return YubiKeyInterfaceUSB::instance()->challenge(slot, challenge, response);
// If a USB key was not found, try PC/SC interface
if (ret == ChallengeResult::YCR_KEYNOTFOUND) {
ret = YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
if (ret == ChallengeResult::YCR_ERROR) {
m_error = YubiKeyInterfacePCSC::instance()->errorMessage();
return ret;
}
}
if (m_pcscKeys.contains(slot)) {
return YubiKeyInterfacePCSC::instance()->challenge(slot, challenge, response);
if (ret == ChallengeResult::YCR_KEYNOTFOUND) {
m_error =
tr("Could not find hardware key with serial number %1. Please connect it to continue.").arg(slot.first);
}
m_error = tr("Could not find interface for hardware key with serial number %1. Please connect it to continue.")
.arg(slot.first);
return ChallengeResult::YCR_ERROR;
return ret;
}

View File

@@ -44,7 +44,8 @@ public:
{
YCR_ERROR = 0,
YCR_SUCCESS = 1,
YCR_WOULDBLOCK = 2
YCR_WOULDBLOCK = 2,
YCR_KEYNOTFOUND = 3,
};
static YubiKey* instance();
@@ -84,14 +85,14 @@ signals:
private:
explicit YubiKey();
void findValidKeys(const QMutexLocker& locker);
static YubiKey* m_instance;
QTimer m_interactionTimer;
bool m_initialized = false;
bool m_findingKeys = false;
QString m_error;
// Prevents multiple simultaneous operations on hardware keys
static QMutex s_interfaceMutex;
KeyMap m_usbKeys;

View File

@@ -679,7 +679,7 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
m_error.clear();
if (!m_initialized) {
m_error = tr("The YubiKey PC/SC interface has not been initialized.");
return YubiKey::ChallengeResult::YCR_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
// Try for a few seconds to find the key
@@ -710,11 +710,8 @@ YubiKeyInterfacePCSC::challenge(YubiKeySlot slot, const QByteArray& challenge, B
}
}
m_error = tr("Could not find or access hardware key with serial number %1. Please present it to continue. ")
.arg(slot.first)
+ m_error;
emit challengeCompleted();
return YubiKey::ChallengeResult::YCR_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
YubiKey::ChallengeResult YubiKeyInterfacePCSC::performChallenge(void* key,

View File

@@ -237,7 +237,7 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
m_error.clear();
if (!m_initialized) {
m_error = tr("The YubiKey USB interface has not been initialized.");
return YubiKey::ChallengeResult::YCR_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
auto yk_key = openKeySerial(slot.first);
@@ -245,12 +245,11 @@ YubiKeyInterfaceUSB::challenge(YubiKeySlot slot, const QByteArray& challenge, Bo
// Key with specified serial number is not connected
m_error =
tr("Could not find hardware key with serial number %1. Please plug it in to continue.").arg(slot.first);
return YubiKey::ChallengeResult::YCR_ERROR;
return YubiKey::ChallengeResult::YCR_KEYNOTFOUND;
}
emit challengeStarted();
auto ret = performChallenge(yk_key.get(), slot.second, true, challenge, response);
emit challengeCompleted();
return ret;

View File

@@ -626,6 +626,17 @@ void TestEntry::testResolveReplacePlaceholders()
// Test complicated and nested replacements
QCOMPARE(entry2->resolveMultiplePlaceholders(entry2->url()),
QString("cmd://sap.exe -system=server1 -client=12345 -user=Username2 -pw=Password1"));
auto* entry3 = new Entry();
entry3->setGroup(root);
entry3->setUuid(QUuid::createUuid());
entry3->setTitle("Entry 3");
entry3->setUsername("HMAC-SHA-256");
entry3->setUrl("{T-REPLACE-RX:!{USERNAME}!\\{USERNAME\\}!!}");
// Test escaped enclosures
QCOMPARE(entry3->resolveMultiplePlaceholders(entry3->url()), entry3->username());
// Test invalid syntax
QString error;
entry1->resolveRegexPlaceholder("{T-REPLACE-RX:/{USERNAME}/.*+?/test/}", &error); // invalid regex

View File

@@ -451,3 +451,14 @@ void TestTools::testCleanUsername_data()
QTest::newRow("Trailing dots and spaces") << "username... " << "username";
QTest::newRow("Combination of issues") << R"( user<>:"/\|?*name... )" << "user_________name";
}
void TestTools::testEscapeAccelerators()
{
QCOMPARE(Tools::escapeAccelerators(""), "");
QCOMPARE(Tools::escapeAccelerators("NoAccelerator"), "NoAccelerator");
QCOMPARE(Tools::escapeAccelerators("&Accelerator"), "&&Accelerator");
QCOMPARE(Tools::escapeAccelerators("Accelerator&"), "Accelerator&&");
QCOMPARE(Tools::escapeAccelerators("Accel&erator&"), "Accel&&erator&&");
QCOMPARE(Tools::escapeAccelerators("Accel&&erator"), "Accel&&&&erator");
QCOMPARE(Tools::escapeAccelerators("Some & text"), "Some && text");
}

View File

@@ -43,6 +43,7 @@ private slots:
void testIsTextMimeType();
void testCleanUsername();
void testCleanUsername_data();
void testEscapeAccelerators();
};
#endif // KEEPASSX_TESTTOOLS_H

View File

@@ -1,77 +1,70 @@
#!/usr/bin/env python3
from collections import defaultdict
import json
import os
import sys
from pathlib import Path
from urllib import request
# Download Transifex languages dump at: https://www.transifex.com/api/2/project/keepassxc/languages
# Language information from https://www.wikiwand.com/en/List_of_ISO_639-1_codes and http://www.lingoes.net/en/translator/langcode.htm
txrc = Path.home() / '.transifexrc'
if not txrc.exists():
print('No Transifex config found. Run tx init first.')
sys.exit(1)
LANGS = {
"ar" : "العربية (Arabic)",
"bn" : "বাংলা (Bengali)",
"ca" : "català (Catalan)",
"cs" : "čeština (Czech)",
"da" : "dansk (Danish)",
"de" : "Deutsch (German)",
"el" : "ελληνικά (Greek)",
"eo" : "Esperanto (Esperanto)",
"es" : "Español (Spanish)",
"et" : "eesti (Estonian)",
"eu" : "euskara (Basque)",
"fa" : "فارسی (Farsi)",
"fa_IR" : "فارسی (Farsi (Iran))",
"fi" : "suomi (Finnish)",
"fr" : "français (French)",
"gl" : "Galego (Galician)",
"he" : "עברית (Hebrew)",
"hr_HR" : "hrvatski jezik (Croatian)",
"hu" : "magyar (Hungarian)",
"id" : "Bahasa Indonesia (Indonesian)",
"is_IS" : "Íslenska (Icelandic)",
"it" : "Italiano (Italian)",
"ja" : "日本語 (Japanese)",
"kk" : "қазақ тілі (Kazakh)",
"ko" : "한국어 (Korean)",
"la" : "latine (Latin)",
"lt" : "lietuvių kalba (Lithuanian)",
"lv" : "latviešu valoda (Latvian)",
"nb" : "Norsk Bokmål (Norwegian Bokmål)",
"nl_NL" : "Nederlands (Dutch)",
"my" : "ဗမာစာ (Burmese)",
"pa" : "ਪੰਜਾਬੀ (Punjabi)",
"pa_IN" : "ਪੰਜਾਬੀ (Punjabi (India))",
"pl" : "język polski (Polish)",
"pt" : "Português (Portuguese)",
"pt_BR" : "Português (Portuguese (Brazil))",
"pt_PT" : "Português (Portuguese (Portugal))",
"ro" : "Română (Romanian)",
"ru" : "русский (Russian)",
"sk" : "Slovenčina (Slovak)",
"sl_SI" : "Slovenščina (Slovenian)",
"sr" : "српски језик (Serbian)",
"sv" : "Svenska (Swedish)",
"th" : "ไทย (Thai)",
"tr" : "Türkçe (Turkish)",
"uk" : "Українська (Ukrainian)",
"zh_CN" : "中文 (Chinese (Simplified))",
"zh_TW" : "中文 (台灣) (Chinese (Traditional))",
}
org = 'o:keepassxc'
proj = f'{org}:p:keepassxc'
resource = f'{proj}:r:share-translations-keepassxc-en-ts--master'
token = [l for l in open(txrc, 'r') if l.startswith('token')][0].split('=', 1)[1].strip()
member_blacklist = ['u:droidmonkey', 'u:phoerious']
TEMPLATE = "<li><strong>{0}</strong>: {1}</li>\n"
if not os.path.exists("languages.json"):
print("Could not find 'languages.json' in current directory!")
print("Save the output from https://www.transifex.com/api/2/project/keepassxc/languages")
exit(0)
def get_url(url):
req = request.Request(url)
req.add_header('Content-Type', 'application/vnd.api+json')
req.add_header('Authorization', f'Bearer {token}')
with request.urlopen(req) as resp:
return json.load(resp)
with open("languages.json") as json_file:
output = open("translators.html", "w", encoding="utf-8")
languages = json.load(json_file)
for lang in languages:
code = lang["language_code"]
if code not in LANGS:
print("WARNING: Could not find language code:", code)
continue
translators = ", ".join(sorted(lang["reviewers"] + lang["translators"], key=str.casefold))
output.write(TEMPLATE.format(LANGS[code], translators))
output.close()
print("Language translators written to 'translators.html'!")
print('Fetching languages...', file=sys.stderr)
languages_json = get_url(f'https://rest.api.transifex.com/projects/{proj}/languages')
languages = {}
for lang in languages_json['data']:
languages[lang['id']] = lang['attributes']['name']
print('Fetching language stats...', file=sys.stderr)
language_stats_json = get_url('https://rest.api.transifex.com/resource_language_stats?'
f'filter[project]={proj}&filter[resource]={resource}')
completion = {}
for stat in language_stats_json['data']:
completion = stat['attributes']['translated_strings'] / stat['attributes']['total_strings']
if completion < .6:
languages.pop(stat['relationships']['language']['data']['id'])
print('Fetching language members...', end='', file=sys.stderr)
members_json = get_url(f'https://rest.api.transifex.com/team_memberships?filter[organization]={org}')
members = defaultdict(set)
for member in members_json['data']:
print('.', end='', file=sys.stderr)
if member['relationships']['user']['data']['id'] in member_blacklist:
continue
lid = member['relationships']['language']['data']['id']
if lid not in languages:
continue
user = get_url(member['relationships']['user']['links']['related'])['data']['attributes']['username']
members[lid].add(user)
print(file=sys.stderr)
print('<ul>')
for lang in sorted(languages, key=lambda x: languages[x]):
if not members[lang]:
continue
lines = [f' <li><strong>{languages[lang]}:</strong> ']
for i, m in enumerate(sorted(members[lang], key=lambda x: x.lower())):
if len(lines[-1]) + len(m) >= 120:
lines.append(' ')
lines[-1] += m
if i < len(members[lang]) - 1:
lines[-1] += ', '
lines[-1] += '</li>'
print('\n'.join(lines))
print('</ul>')