diff --git a/src/Mod/AddonManager/Addon.py b/src/Mod/AddonManager/Addon.py
index 59a360b6e3..9af58937dd 100644
--- a/src/Mod/AddonManager/Addon.py
+++ b/src/Mod/AddonManager/Addon.py
@@ -227,6 +227,8 @@ class Addon:
self._cached_license = self.metadata.license
elif self.stats and self.stats.license:
self._cached_license = self.stats.license
+ elif self.macro and self.macro.license:
+ self._cached_license = self.macro.license
return self._cached_license
@classmethod
diff --git a/src/Mod/AddonManager/NetworkManager.py b/src/Mod/AddonManager/NetworkManager.py
index d3481187f8..98a1be16be 100644
--- a/src/Mod/AddonManager/NetworkManager.py
+++ b/src/Mod/AddonManager/NetworkManager.py
@@ -315,7 +315,11 @@ if HAVE_QTNETWORK:
reply.readyRead.connect(self.__ready_to_read)
reply.downloadProgress.connect(self.__download_progress)
- def submit_unmonitored_get(self, url: str) -> int:
+ def submit_unmonitored_get(
+ self,
+ url: str,
+ timeout_ms: int = QtNetwork.QNetworkRequest.DefaultTransferTimeoutConstant,
+ ) -> int:
"""Adds this request to the queue, and returns an index that can be used by calling code
in conjunction with the completed() signal to handle the results of the call. All data is
kept in memory, and the completed() call includes a direct handle to the bytes returned. It
@@ -324,12 +328,18 @@ if HAVE_QTNETWORK:
current_index = next(self.counting_iterator) # A thread-safe counter
# Use a queue because we can only put things on the QNAM from the main event loop thread
self.queue.put(
- QueueItem(current_index, self.__create_get_request(url), track_progress=False)
+ QueueItem(
+ current_index, self.__create_get_request(url, timeout_ms), track_progress=False
+ )
)
self.__request_queued.emit()
return current_index
- def submit_monitored_get(self, url: str) -> int:
+ def submit_monitored_get(
+ self,
+ url: str,
+ timeout_ms: int = QtNetwork.QNetworkRequest.DefaultTransferTimeoutConstant,
+ ) -> int:
"""Adds this request to the queue, and returns an index that can be used by calling code
in conjunction with the progress_made() and progress_completed() signals to handle the
results of the call. All data is cached to disk, and progress is reported periodically
@@ -340,12 +350,18 @@ if HAVE_QTNETWORK:
current_index = next(self.counting_iterator) # A thread-safe counter
# Use a queue because we can only put things on the QNAM from the main event loop thread
self.queue.put(
- QueueItem(current_index, self.__create_get_request(url), track_progress=True)
+ QueueItem(
+ current_index, self.__create_get_request(url, timeout_ms), track_progress=True
+ )
)
self.__request_queued.emit()
return current_index
- def blocking_get(self, url: str) -> Optional[QtCore.QByteArray]:
+ def blocking_get(
+ self,
+ url: str,
+ timeout_ms: int = QtNetwork.QNetworkRequest.DefaultTransferTimeoutConstant,
+ ) -> Optional[QtCore.QByteArray]:
"""Submits a GET request to the QNetworkAccessManager and block until it is complete"""
current_index = next(self.counting_iterator) # A thread-safe counter
@@ -353,7 +369,9 @@ if HAVE_QTNETWORK:
self.synchronous_complete[current_index] = False
self.queue.put(
- QueueItem(current_index, self.__create_get_request(url), track_progress=False)
+ QueueItem(
+ current_index, self.__create_get_request(url, timeout_ms), track_progress=False
+ )
)
self.__request_queued.emit()
while True:
@@ -388,7 +406,7 @@ if HAVE_QTNETWORK:
)
self.synchronous_complete[index] = True
- def __create_get_request(self, url: str) -> QtNetwork.QNetworkRequest:
+ def __create_get_request(self, url: str, timeout_ms: int) -> QtNetwork.QNetworkRequest:
"""Construct a network request to a given URL"""
request = QtNetwork.QNetworkRequest(QtCore.QUrl(url))
request.setAttribute(
@@ -400,6 +418,7 @@ if HAVE_QTNETWORK:
QtNetwork.QNetworkRequest.CacheLoadControlAttribute,
QtNetwork.QNetworkRequest.PreferNetwork,
)
+ request.setTransferTimeout(timeout_ms)
return request
def abort_all(self):
@@ -428,7 +447,8 @@ if HAVE_QTNETWORK:
authenticator: QtNetwork.QAuthenticator,
):
"""If proxy authentication is required, attempt to authenticate. If the GUI is running this displays
- a window asking for credentials. If the GUI is not running, it prompts on the command line."""
+ a window asking for credentials. If the GUI is not running, it prompts on the command line.
+ """
if HAVE_FREECAD and FreeCAD.GuiUp:
proxy_authentication = FreeCADGui.PySideUic.loadUi(
os.path.join(os.path.dirname(__file__), "proxy_authentication.ui")
@@ -463,6 +483,9 @@ if HAVE_QTNETWORK:
def __follow_redirect(self, url):
"""Used with the QNetworkAccessManager to follow redirects."""
sender = self.sender()
+ current_index = -1
+ timeout_ms = QtNetwork.QNetworkRequest.DefaultTransferTimeoutConstant
+ # TODO: Figure out what the actual timeout value should be from the original request
if sender:
for index, reply in self.replies.items():
if reply == sender:
@@ -470,7 +493,8 @@ if HAVE_QTNETWORK:
break
sender.abort()
- self.__launch_request(current_index, self.__create_get_request(url))
+ if current_index != -1:
+ self.__launch_request(current_index, self.__create_get_request(url, timeout_ms))
def __on_ssl_error(self, reply: str, errors: List[str] = None):
"""Called when an SSL error occurs: prints the error information."""
@@ -620,7 +644,6 @@ def InitializeNetworkManager():
if __name__ == "__main__":
-
app = QtCore.QCoreApplication()
InitializeNetworkManager()
diff --git a/src/Mod/AddonManager/Resources/AddonManager.qrc b/src/Mod/AddonManager/Resources/AddonManager.qrc
index a1f12a12d8..a2a0b75b85 100644
--- a/src/Mod/AddonManager/Resources/AddonManager.qrc
+++ b/src/Mod/AddonManager/Resources/AddonManager.qrc
@@ -69,11 +69,11 @@
(.*?)", p.replace("\n", "--endl--")) if code: # take the biggest code block - code = sorted(code, key=len)[-1] + code = str(sorted(code, key=len)[-1]) code = code.replace("--endl--", "\n") # Clean HTML escape codes. code = unescape(code) @@ -327,7 +331,7 @@ class Macro: self.other_files.append(self.icon) def _copy_other_files(self, macro_dir, warnings) -> bool: - """Copy any specified "other files" into the install directory""" + """Copy any specified "other files" into the installation directory""" base_dir = os.path.dirname(self.src_filename) for other_file in self.other_files: if not other_file: @@ -382,7 +386,7 @@ class Macro: ) def parse_wiki_page_for_icon(self, page_data: str) -> None: - """Attempt to find a url for the icon in the wiki page. Sets self.icon if + """Attempt to find the url for the icon in the wiki page. Sets 'self.icon' if found.""" # Method 1: the text "toolbar icon" appears on the page, and provides a direct diff --git a/src/Mod/AddonManager/addonmanager_macro_parser.py b/src/Mod/AddonManager/addonmanager_macro_parser.py index 86d829bbe7..5c77037574 100644 --- a/src/Mod/AddonManager/addonmanager_macro_parser.py +++ b/src/Mod/AddonManager/addonmanager_macro_parser.py @@ -36,8 +36,10 @@ except ImportError: try: import FreeCAD + from addonmanager_licenses import get_license_manager except ImportError: FreeCAD = None + get_license_manager = None class DummyThread: @@ -63,6 +65,7 @@ class MacroParser: "other_files": [""], "author": "", "date": "", + "license": "", "icon": "", "xpm": "", } @@ -83,6 +86,8 @@ class MacroParser: "__files__": "other_files", "__author__": "author", "__date__": "date", + "__license__": "license", + "__licence__": "license", # accept either spelling "__icon__": "icon", "__xpm__": "xpm", } @@ -185,6 +190,8 @@ class MacroParser: self.parse_results[value] = match_group if value == "comment": self._cleanup_comment() + elif value == "license": + self._cleanup_license() elif isinstance(self.parse_results[value], list): self.parse_results[value] = [of.strip() for of in match_group.split(",")] else: @@ -197,6 +204,11 @@ class MacroParser: if len(self.parse_results["comment"]) > 512: self.parse_results["comment"] = self.parse_results["comment"][:511] + "…" + def _cleanup_license(self): + if get_license_manager is not None: + lm = get_license_manager() + self.parse_results["license"] = lm.normalize(self.parse_results["license"]) + def _apply_special_handling(self, key: str, line: str): # Macro authors are supposed to be providing strings here, but in some # cases they are not doing so. If this is the "__version__" tag, try diff --git a/src/Mod/AddonManager/addonmanager_metadata.py b/src/Mod/AddonManager/addonmanager_metadata.py index 213b0e2c5e..1afb733454 100644 --- a/src/Mod/AddonManager/addonmanager_metadata.py +++ b/src/Mod/AddonManager/addonmanager_metadata.py @@ -30,6 +30,9 @@ from dataclasses import dataclass, field from enum import IntEnum, auto from typing import Tuple, Dict, List, Optional +from addonmanager_licenses import get_license_manager +import addonmanager_freecad_interface as fci + try: # If this system provides a secure parser, use that: import defusedxml.ElementTree as ET @@ -315,7 +318,10 @@ class MetadataReader: @staticmethod def _parse_license(child: ET.Element) -> License: file = child.attrib["file"] if "file" in child.attrib else "" - return License(name=child.text, file=file) + license_id = child.text + lm = get_license_manager() + license_id = lm.normalize(license_id) + return License(name=license_id, file=file) @staticmethod def _parse_url(child: ET.Element) -> Url: diff --git a/src/Mod/AddonManager/addonmanager_workers_utility.py b/src/Mod/AddonManager/addonmanager_workers_utility.py index eaa8e5c67e..48b8d360bd 100644 --- a/src/Mod/AddonManager/addonmanager_workers_utility.py +++ b/src/Mod/AddonManager/addonmanager_workers_utility.py @@ -55,7 +55,9 @@ class ConnectionChecker(QtCore.QThread): url = "https://api.github.com/zen" self.done = False NetworkManager.AM_NETWORK_MANAGER.completed.connect(self.connection_data_received) - self.request_id = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get(url) + self.request_id = NetworkManager.AM_NETWORK_MANAGER.submit_unmonitored_get( + url, timeout_ms=10000 + ) while not self.done: if QtCore.QThread.currentThread().isInterruptionRequested(): FreeCAD.Console.PrintLog("Connection check cancelled\n") diff --git a/src/Mod/AddonManager/package_list.py b/src/Mod/AddonManager/package_list.py index 3d9f310346..849aa1d457 100644 --- a/src/Mod/AddonManager/package_list.py +++ b/src/Mod/AddonManager/package_list.py @@ -560,12 +560,41 @@ class PackageListFilter(QtCore.QSortFilterProxyModel): return False # If it is not an OSI-approved license, check to see if we are hiding those - if self.hide_non_OSI_approved and not license_manager.is_osi_approved(data.license): - return False + if self.hide_non_OSI_approved or self.hide_non_FSF_libre: + if not data.license: + return False + licenses_to_check = [] + if type(data.license) is str: + licenses_to_check.append(data.license) + elif type(data.license) is list: + for license_id in data.license: + if type(license_id) is str: + licenses_to_check.append(license_id) + else: + licenses_to_check.append(license_id.name) + else: + licenses_to_check.append(data.license.name) - # If it is not an FSF Free/Libre license, check to see if we are hiding those - if self.hide_non_FSF_libre and not license_manager.is_fsf_libre(data.license): - return False + fsf_libre = False + osi_approved = False + for license_id in licenses_to_check: + if not osi_approved and license_manager.is_osi_approved(license_id): + osi_approved = True + if not fsf_libre and license_manager.is_fsf_libre(license_id): + fsf_libre = True + if self.hide_non_OSI_approved and not osi_approved: + FreeCAD.Console.PrintLog( + f"Hiding addon {data.name} because its license, {licenses_to_check}, " + f"is " + f"not OSI approved\n" + ) + return False + if self.hide_non_FSF_libre and not fsf_libre: + FreeCAD.Console.PrintLog( + f"Hiding addon {data.name} because its license, {licenses_to_check}, is " + f"not FSF Libre\n" + ) + return False # If it's not installed, check to see if it's for a newer version of FreeCAD if (