You've already forked qBittorrent
							
							
				mirror of
				https://github.com/qbittorrent/qBittorrent
				synced 2025-10-30 23:30:54 +01:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			release-4.
			...
			release-4.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6486fc5f4d | ||
|   | 1e059ab1a2 | ||
|   | 15b137211b | ||
|   | 6f8f1d7bad | ||
|   | a31f0c0a3d | ||
|   | f977d1293a | ||
|   | 1399be50cb | ||
|   | 52dcf32cc8 | ||
|   | 52b2b807ab | ||
|   | 5cf4f00824 | ||
|   | faa6fad025 | ||
|   | 9f94bbce3a | ||
|   | 5c49b2486c | ||
|   | 4f6e7f97c6 | ||
|   | 7751c5b75c | ||
|   | a1a9f3317b | ||
|   | fb20f59a96 | ||
|   | a15e3407b0 | ||
|   | e267c2d37a | ||
|   | ae32edeb26 | ||
|   | 34d38ef466 | ||
|   | 120ee6b836 | ||
|   | 7d25b6fce2 | ||
|   | 068eff9e9f | ||
|   | 31a55f79f1 | ||
|   | bac032e01c | ||
|   | b809941f02 | ||
|   | 77c3758090 | ||
|   | 5758817189 | ||
|   | acc9f08a05 | ||
|   | f3b7f17a7c | ||
|   | dfc3f047e2 | ||
|   | 223ab7de84 | ||
|   | d2a4027347 | 
							
								
								
									
										18
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,14 +1,20 @@ | ||||
| **Please provide the following information** | ||||
|  | ||||
| ### qBittorrent version and Operating System: | ||||
| ### qBittorrent version and Operating System | ||||
| (type here) | ||||
|  | ||||
| ### If on linux, libtorrent and Qt version: | ||||
| ### If on linux, libtorrent and Qt version | ||||
| (type here) | ||||
|  | ||||
| ### What is the problem: | ||||
| ### What is the problem | ||||
| (type here) | ||||
|  | ||||
| ### What is the expected behavior: | ||||
| ### What is the expected behavior | ||||
| (type here) | ||||
|  | ||||
| ### Steps to reproduce: | ||||
| ### Steps to reproduce | ||||
| (type here) | ||||
|  | ||||
| ### Extra info(if any): | ||||
| ### Extra info(if any) | ||||
| (type here) | ||||
|  | ||||
|   | ||||
							
								
								
									
										24
									
								
								Changelog
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								Changelog
									
									
									
									
									
								
							| @@ -1,3 +1,27 @@ | ||||
| * Fri Dec 01 2017 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v4.0.2 | ||||
|     - BUGFIX: Fix crash on some systems when creating address object for 255.255.255.255. Closes #7735. (sledgehammer999) | ||||
|     - PERFORMANCE: Change MixedModeAlgorithm default to TCP. This was the v3_3_x default and should sustain higher speeds. Closes #7779. (Chocobo1) | ||||
|     - PERFORMANCE: Stop logging IP filter parsing errors after a while, otherwise the GUI freezes or qBittorrent doesn't start. (sledgehammer999) | ||||
|     - GUI: Implement stable sort. Rows in transfer list shouldn't flicker anymore. (Chocobo1) | ||||
|     - WEBUI: Fix build when webui is disabled. (Heiko Becker) | ||||
|     - RSS: Fix build because of missing header. Closes #7805. (thoradia) | ||||
|     - RSS: Fix RSS parser. (glassez) | ||||
|     - RSS: Implement Import/Export RSS rules in legacy(aka v3_3_x) format. (glassez) | ||||
|     - RSS: Implement Import/Export RSS rules in JSON format. (glassez) | ||||
|     - WINDOWS: Fixed blurry text under Windows by setting DPI awareness to default. (TheNicker) | ||||
|     - LINUX: Fix i386 build. (Evgeny Lensky) | ||||
|  | ||||
| * Wed Nov 22 2017 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v4.0.1 | ||||
|     - BUGFIX: Fix crash on opening torrent/magnet (uninitialized pointer). Closes #7739 #7723. (sledgehammer999) | ||||
|     - BUGFIX: Enable preferences Apply button when ip banlist is modified (Thomas Piccirello) | ||||
|     - BUGFIX: Allow drag-n-drop magnet links to mainwindow. Closes #7742. (Chocobo1) | ||||
|     - BUGFIX: Fix crash when aborting a torrent creation process. Closes #7783. (Chocobo1) | ||||
|     - BUGFIX: Correctly check if torrent passed during application start already exists. (sledgehammer999) | ||||
|     - WEBUI: Add ip subnet whitelist for bypassing webui auth (Thomas Piccirello) | ||||
|     - WEBUI: Fix logo missing in login page (Chocobo1) | ||||
|     - COSMETIC: Fix english typo. (sledgehammer999) | ||||
|     - OTHER: cmake: qtsingleapplication should always be built statically (luigino) | ||||
|  | ||||
| * Mon Nov 20 2017 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v4.0.0 | ||||
|     - FEATURE: Change qbittorrent logo. Issue #6467. (HVS, Atif Afzal, sledgehammer999) | ||||
|     - FEATURE: New icon theme with SVG source, so we can scale it appropriately in the future. (Bert Verhelst) | ||||
|   | ||||
							
								
								
									
										17
									
								
								configure
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								configure
									
									
									
									
										vendored
									
									
								
							| @@ -690,7 +690,6 @@ infodir | ||||
| docdir | ||||
| oldincludedir | ||||
| includedir | ||||
| runstatedir | ||||
| localstatedir | ||||
| sharedstatedir | ||||
| sysconfdir | ||||
| @@ -784,7 +783,6 @@ datadir='${datarootdir}' | ||||
| sysconfdir='${prefix}/etc' | ||||
| sharedstatedir='${prefix}/com' | ||||
| localstatedir='${prefix}/var' | ||||
| runstatedir='${localstatedir}/run' | ||||
| includedir='${prefix}/include' | ||||
| oldincludedir='/usr/include' | ||||
| docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' | ||||
| @@ -1037,15 +1035,6 @@ do | ||||
|   | -silent | --silent | --silen | --sile | --sil) | ||||
|     silent=yes ;; | ||||
|  | ||||
|   -runstatedir | --runstatedir | --runstatedi | --runstated \ | ||||
|   | --runstate | --runstat | --runsta | --runst | --runs \ | ||||
|   | --run | --ru | --r) | ||||
|     ac_prev=runstatedir ;; | ||||
|   -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ | ||||
|   | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ | ||||
|   | --run=* | --ru=* | --r=*) | ||||
|     runstatedir=$ac_optarg ;; | ||||
|  | ||||
|   -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) | ||||
|     ac_prev=sbindir ;; | ||||
|   -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ | ||||
| @@ -1183,7 +1172,7 @@ fi | ||||
| for ac_var in	exec_prefix prefix bindir sbindir libexecdir datarootdir \ | ||||
| 		datadir sysconfdir sharedstatedir localstatedir includedir \ | ||||
| 		oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ | ||||
| 		libdir localedir mandir runstatedir | ||||
| 		libdir localedir mandir | ||||
| do | ||||
|   eval ac_val=\$$ac_var | ||||
|   # Remove trailing slashes. | ||||
| @@ -1336,7 +1325,6 @@ Fine tuning of the installation directories: | ||||
|   --sysconfdir=DIR        read-only single-machine data [PREFIX/etc] | ||||
|   --sharedstatedir=DIR    modifiable architecture-independent data [PREFIX/com] | ||||
|   --localstatedir=DIR     modifiable single-machine data [PREFIX/var] | ||||
|   --runstatedir=DIR       modifiable per-process data [LOCALSTATEDIR/run] | ||||
|   --libdir=DIR            object code libraries [EPREFIX/lib] | ||||
|   --includedir=DIR        C header files [PREFIX/include] | ||||
|   --oldincludedir=DIR     C header files for non-gcc [/usr/include] | ||||
| @@ -4705,9 +4693,8 @@ fi | ||||
|     libsubdirs="lib64 libx32 lib lib64" ;; #( | ||||
|   ppc64|s390x|sparc64|aarch64|ppc64le) : | ||||
|     libsubdirs="lib64 lib lib64" ;; #( | ||||
|   libsubdirs="lib") : | ||||
|      ;; #( | ||||
|   *) : | ||||
|     libsubdirs="lib" | ||||
|      ;; | ||||
| esac | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								dist/mac/Info.plist
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/mac/Info.plist
									
									
									
									
										vendored
									
									
								
							| @@ -45,7 +45,7 @@ | ||||
| 	<key>CFBundlePackageType</key> | ||||
| 	<string>APPL</string> | ||||
| 	<key>CFBundleShortVersionString</key> | ||||
| 	<string>4.0.0</string> | ||||
| 	<string>4.0.2</string> | ||||
| 	<key>CFBundleSignature</key> | ||||
| 	<string>qBit</string> | ||||
| 	<key>CFBundleExecutable</key> | ||||
|   | ||||
							
								
								
									
										2
									
								
								dist/windows/options.nsi
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/windows/options.nsi
									
									
									
									
										vendored
									
									
								
							| @@ -27,7 +27,7 @@ XPStyle on | ||||
| !define CSIDL_LOCALAPPDATA '0x1C' ;Local Application Data path | ||||
|  | ||||
| ; Program specific | ||||
| !define PROG_VERSION "4.0.0" | ||||
| !define PROG_VERSION "4.0.2" | ||||
|  | ||||
| !define MUI_FINISHPAGE_RUN | ||||
| !define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun | ||||
|   | ||||
							
								
								
									
										2
									
								
								dist/windows/qt.conf
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/windows/qt.conf
									
									
									
									
										vendored
									
									
								
							| @@ -2,4 +2,4 @@ | ||||
| Translations = translations | ||||
|  | ||||
| [Platforms] | ||||
| WindowsArguments = dpiawareness=1 | ||||
| ;WindowsArguments = dpiawareness=1 | ||||
|   | ||||
| @@ -114,7 +114,7 @@ AC_DEFUN([_AX_BOOST_BASE_RUNDETECT],[ | ||||
|     AS_CASE([${host_cpu}], | ||||
|       [x86_64],[libsubdirs="lib64 libx32 lib lib64"], | ||||
|       [ppc64|s390x|sparc64|aarch64|ppc64le],[libsubdirs="lib64 lib lib64"], | ||||
|       [libsubdirs="lib"], | ||||
|       [libsubdirs="lib"] | ||||
|     ) | ||||
|  | ||||
|     dnl allow for real multi-arch paths e.g. /usr/lib/x86_64-linux-gnu. Give | ||||
|   | ||||
| @@ -105,6 +105,9 @@ Application::Application(const QString &id, int &argc, char **argv) | ||||
|     , m_running(false) | ||||
|     , m_shutdownAct(ShutdownDialogAction::Exit) | ||||
|     , m_commandLineArgs(parseCommandLine(this->arguments())) | ||||
| #ifndef DISABLE_WEBUI | ||||
|     , m_webui(nullptr) | ||||
| #endif | ||||
| { | ||||
|     qRegisterMetaType<Log::Msg>("Log::Msg"); | ||||
|  | ||||
| @@ -518,15 +521,15 @@ int Application::exec(const QStringList ¶ms) | ||||
| #endif // DISABLE_GUI | ||||
|  | ||||
|     m_running = true; | ||||
|  | ||||
|     // Now UI is ready to process signals from Session | ||||
|     BitTorrent::Session::instance()->startUpTorrents(); | ||||
|  | ||||
|     m_paramsQueue = params + m_paramsQueue; | ||||
|     if (!m_paramsQueue.isEmpty()) { | ||||
|         processParams(m_paramsQueue); | ||||
|         m_paramsQueue.clear(); | ||||
|     } | ||||
|  | ||||
|     // Now UI is ready to process signals from Session | ||||
|     BitTorrent::Session::instance()->startUpTorrents(); | ||||
|  | ||||
|     return BaseApplication::exec(); | ||||
| } | ||||
|  | ||||
| @@ -599,15 +602,15 @@ void Application::initializeTranslation() | ||||
|     // Load translation | ||||
|     QString localeStr = pref->getLocale(); | ||||
|  | ||||
|     if (m_qtTranslator.load(QString::fromUtf8("qtbase_") + localeStr, QLibraryInfo::location(QLibraryInfo::TranslationsPath)) || | ||||
|         m_qtTranslator.load(QString::fromUtf8("qt_") + localeStr, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) | ||||
|     if (m_qtTranslator.load(QLatin1String("qtbase_") + localeStr, QLibraryInfo::location(QLibraryInfo::TranslationsPath)) || | ||||
|         m_qtTranslator.load(QLatin1String("qt_") + localeStr, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) | ||||
|             qDebug("Qt %s locale recognized, using translation.", qUtf8Printable(localeStr)); | ||||
|     else | ||||
|         qDebug("Qt %s locale unrecognized, using default (en).", qUtf8Printable(localeStr)); | ||||
|  | ||||
|     installTranslator(&m_qtTranslator); | ||||
|  | ||||
|     if (m_translator.load(QString::fromUtf8(":/lang/qbittorrent_") + localeStr)) | ||||
|     if (m_translator.load(QLatin1String(":/lang/qbittorrent_") + localeStr)) | ||||
|         qDebug("%s locale recognized, using translation.", qUtf8Printable(localeStr)); | ||||
|     else | ||||
|         qDebug("%s locale unrecognized, using default (en).", qUtf8Printable(localeStr)); | ||||
|   | ||||
| @@ -16,7 +16,7 @@ else (GUI) | ||||
|     list(APPEND QBT_QTSINGLEAPPLICATION_SOURCES qtsinglecoreapplication.cpp) | ||||
| endif (GUI) | ||||
|  | ||||
| add_library(qtsingleapplication ${QBT_QTSINGLEAPPLICATION_HEADERS} ${QBT_QTSINGLEAPPLICATION_SOURCES}) | ||||
| add_library(qtsingleapplication STATIC ${QBT_QTSINGLEAPPLICATION_HEADERS} ${QBT_QTSINGLEAPPLICATION_SOURCES}) | ||||
| target_include_directories(qtsingleapplication INTERFACE "${qtsingleapplication_SOURCE_DIR}") | ||||
| target_link_qt_components(qtsingleapplication Network) | ||||
|  | ||||
|   | ||||
| @@ -102,6 +102,7 @@ namespace | ||||
|     } | ||||
|  | ||||
|     const int BUFFER_SIZE = 2 * 1024 * 1024; // 2 MiB | ||||
|     const int MAX_LOGGED_ERRORS = 5; | ||||
| } | ||||
|  | ||||
| FilterParserThread::FilterParserThread(QObject *parent) | ||||
| @@ -134,6 +135,12 @@ int FilterParserThread::parseDATFilterFile() | ||||
|     int start = 0; | ||||
|     int endOfLine = -1; | ||||
|     int nbLine = 0; | ||||
|     int parseErrorCount = 0; | ||||
|     const auto addLog = [&parseErrorCount](const QString &msg) | ||||
|     { | ||||
|         if (parseErrorCount <= MAX_LOGGED_ERRORS) | ||||
|             LogMsg(msg, Log::CRITICAL); | ||||
|     }; | ||||
|  | ||||
|     while (true) { | ||||
|         bytesRead = file.read(buffer.data() + offset, BUFFER_SIZE - offset - 1); | ||||
| @@ -202,7 +209,8 @@ int FilterParserThread::parseDATFilterFile() | ||||
|             int endOfIPRange = ((firstComma == -1) ? (endOfLine - 1) : (firstComma - 1)); | ||||
|             int delimIP = findAndNullDelimiter(buffer.data(), '-', start, endOfIPRange); | ||||
|             if (delimIP == -1) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -210,7 +218,8 @@ int FilterParserThread::parseDATFilterFile() | ||||
|             libt::address startAddr; | ||||
|             int newStart = trim(buffer.data(), start, delimIP - 1); | ||||
|             if (!parseIPAddress(buffer.data() + newStart, startAddr)) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed. Start IP of the range is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed. Start IP of the range is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -218,14 +227,16 @@ int FilterParserThread::parseDATFilterFile() | ||||
|             libt::address endAddr; | ||||
|             newStart = trim(buffer.data(), delimIP + 1, endOfIPRange); | ||||
|             if (!parseIPAddress(buffer.data() + newStart, endAddr)) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed. End IP of the range is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed. End IP of the range is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if ((startAddr.is_v4() != endAddr.is_v4()) | ||||
|                 || (startAddr.is_v6() != endAddr.is_v6())) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed. One IP is IPv4 and the other is IPv6!").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed. One IP is IPv4 and the other is IPv6!").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -238,8 +249,9 @@ int FilterParserThread::parseDATFilterFile() | ||||
|                 ++ruleCount; | ||||
|             } | ||||
|             catch (std::exception &e) { | ||||
|                 LogMsg(tr("IP filter exception thrown for line %1. Exception is: %2").arg(nbLine) | ||||
|                                                .arg(QString::fromLocal8Bit(e.what())), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter exception thrown for line %1. Exception is: %2") | ||||
|                        .arg(nbLine).arg(QString::fromLocal8Bit(e.what()))); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -247,6 +259,9 @@ int FilterParserThread::parseDATFilterFile() | ||||
|             offset = 0; | ||||
|     } | ||||
|  | ||||
|     if (parseErrorCount > MAX_LOGGED_ERRORS) | ||||
|         LogMsg(tr("%1 extra IP filter parsing errors occurred.", "513 extra IP filter parsing errors occurred.") | ||||
|                .arg(parseErrorCount - MAX_LOGGED_ERRORS), Log::CRITICAL); | ||||
|     return ruleCount; | ||||
| } | ||||
|  | ||||
| @@ -268,6 +283,12 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|     int start = 0; | ||||
|     int endOfLine = -1; | ||||
|     int nbLine = 0; | ||||
|     int parseErrorCount = 0; | ||||
|     const auto addLog = [&parseErrorCount](const QString &msg) | ||||
|     { | ||||
|         if (parseErrorCount <= MAX_LOGGED_ERRORS) | ||||
|             LogMsg(msg, Log::CRITICAL); | ||||
|     }; | ||||
|  | ||||
|     while (true) { | ||||
|         bytesRead = file.read(buffer.data() + offset, BUFFER_SIZE - offset - 1); | ||||
| @@ -319,7 +340,8 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|             // The "Some organization" part might contain a ':' char itself so we find the last occurrence | ||||
|             int partsDelimiter = findAndNullDelimiter(buffer.data(), ':', start, endOfLine, true); | ||||
|             if (partsDelimiter == -1) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -327,7 +349,8 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|             // IP Range should be split by a dash | ||||
|             int delimIP = findAndNullDelimiter(buffer.data(), '-', partsDelimiter + 1, endOfLine); | ||||
|             if (delimIP == -1) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -335,7 +358,8 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|             libt::address startAddr; | ||||
|             int newStart = trim(buffer.data(), partsDelimiter + 1, delimIP - 1); | ||||
|             if (!parseIPAddress(buffer.data() + newStart, startAddr)) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed. Start IP of the range is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed. Start IP of the range is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -343,14 +367,16 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|             libt::address endAddr; | ||||
|             newStart = trim(buffer.data(), delimIP + 1, endOfLine); | ||||
|             if (!parseIPAddress(buffer.data() + newStart, endAddr)) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed. End IP of the range is malformed.").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed. End IP of the range is malformed.").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if ((startAddr.is_v4() != endAddr.is_v4()) | ||||
|                 || (startAddr.is_v6() != endAddr.is_v6())) { | ||||
|                 LogMsg(tr("IP filter line %1 is malformed. One IP is IPv4 and the other is IPv6!").arg(nbLine), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter line %1 is malformed. One IP is IPv4 and the other is IPv6!").arg(nbLine)); | ||||
|                 start = endOfLine; | ||||
|                 continue; | ||||
|             } | ||||
| @@ -362,8 +388,9 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|                 ++ruleCount; | ||||
|             } | ||||
|             catch (std::exception &e) { | ||||
|                 LogMsg(tr("IP filter exception thrown for line %1. Exception is: %2").arg(nbLine) | ||||
|                        .arg(QString::fromLocal8Bit(e.what())), Log::CRITICAL); | ||||
|                 ++parseErrorCount; | ||||
|                 addLog(tr("IP filter exception thrown for line %1. Exception is: %2") | ||||
|                        .arg(nbLine).arg(QString::fromLocal8Bit(e.what()))); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -371,6 +398,9 @@ int FilterParserThread::parseP2PFilterFile() | ||||
|             offset = 0; | ||||
|     } | ||||
|  | ||||
|     if (parseErrorCount > MAX_LOGGED_ERRORS) | ||||
|         LogMsg(tr("%1 extra IP filter parsing errors occurred.", "513 extra IP filter parsing errors occurred.") | ||||
|                .arg(parseErrorCount - MAX_LOGGED_ERRORS), Log::CRITICAL); | ||||
|     return ruleCount; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -304,7 +304,7 @@ Session::Session(QObject *parent) | ||||
|     , m_btProtocol(BITTORRENT_SESSION_KEY("BTProtocol"), BTProtocol::Both | ||||
|         , clampValue(BTProtocol::Both, BTProtocol::UTP)) | ||||
|     , m_isUTPRateLimited(BITTORRENT_SESSION_KEY("uTPRateLimited"), true) | ||||
|     , m_utpMixedMode(BITTORRENT_SESSION_KEY("uTPMixedMode"), MixedModeAlgorithm::Proportional | ||||
|     , m_utpMixedMode(BITTORRENT_SESSION_KEY("uTPMixedMode"), MixedModeAlgorithm::TCP | ||||
|         , clampValue(MixedModeAlgorithm::TCP, MixedModeAlgorithm::Proportional)) | ||||
|     , m_multiConnectionsPerIpEnabled(BITTORRENT_SESSION_KEY("MultiConnectionsPerIp"), false) | ||||
|     , m_isAddTrackersEnabled(BITTORRENT_SESSION_KEY("AddTrackersEnabled"), false) | ||||
| @@ -1418,8 +1418,11 @@ void Session::configure(libtorrent::settings_pack &settingsPack) | ||||
| void Session::configurePeerClasses() | ||||
| { | ||||
|     libt::ip_filter f; | ||||
|     f.add_rule(libt::address_v4::from_string("0.0.0.0") | ||||
|                , libt::address_v4::from_string("255.255.255.255") | ||||
|     // address_v4::from_string("255.255.255.255") crashes on some people's systems | ||||
|     // so instead we use address_v4::broadcast() | ||||
|     // Proactively do the same for 0.0.0.0 and address_v4::any() | ||||
|     f.add_rule(libt::address_v4::any() | ||||
|                , libt::address_v4::broadcast() | ||||
|                , 1 << libt::session::global_peer_class_id); | ||||
| #if TORRENT_USE_IPV6 | ||||
|     // IPv6 may not be available on OS and the parsing | ||||
|   | ||||
| @@ -35,8 +35,8 @@ | ||||
| #include <boost/bind.hpp> | ||||
| #include <libtorrent/bencode.hpp> | ||||
| #include <libtorrent/create_torrent.hpp> | ||||
| #include <libtorrent/torrent_info.hpp> | ||||
| #include <libtorrent/storage.hpp> | ||||
| #include <libtorrent/torrent_info.hpp> | ||||
|  | ||||
| #include <QFile> | ||||
|  | ||||
| @@ -44,16 +44,19 @@ | ||||
| #include "base/utils/misc.h" | ||||
| #include "base/utils/string.h" | ||||
|  | ||||
| namespace | ||||
| { | ||||
|     // do not include files and folders whose | ||||
|     // name starts with a . | ||||
|     bool fileFilter(const std::string &f) | ||||
|     { | ||||
|         return !Utils::Fs::fileName(QString::fromStdString(f)).startsWith('.'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| namespace libt = libtorrent; | ||||
| using namespace BitTorrent; | ||||
|  | ||||
| // do not include files and folders whose | ||||
| // name starts with a . | ||||
| bool fileFilter(const std::string &f) | ||||
| { | ||||
|     return !Utils::Fs::fileName(QString::fromStdString(f)).startsWith('.'); | ||||
| } | ||||
|  | ||||
| TorrentCreatorThread::TorrentCreatorThread(QObject *parent) | ||||
|     : QThread(parent) | ||||
|     , m_private(false) | ||||
| @@ -64,7 +67,7 @@ TorrentCreatorThread::TorrentCreatorThread(QObject *parent) | ||||
| TorrentCreatorThread::~TorrentCreatorThread() | ||||
| { | ||||
|     requestInterruption(); | ||||
|     wait(1000); | ||||
|     wait(); | ||||
| } | ||||
|  | ||||
| void TorrentCreatorThread::create(const QString &inputPath, const QString &savePath, const QStringList &trackers, | ||||
|   | ||||
| @@ -30,6 +30,8 @@ | ||||
|  * Contact : hammered999@gmail.com | ||||
|  */ | ||||
|  | ||||
| #include "preferences.h" | ||||
|  | ||||
| #include <QCryptographicHash> | ||||
| #include <QDir> | ||||
| #include <QLocale> | ||||
| @@ -51,11 +53,10 @@ | ||||
| #include <CoreServices/CoreServices.h> | ||||
| #endif | ||||
|  | ||||
| #include "logger.h" | ||||
| #include "settingsstorage.h" | ||||
| #include "utils/fs.h" | ||||
| #include "utils/misc.h" | ||||
| #include "settingsstorage.h" | ||||
| #include "logger.h" | ||||
| #include "preferences.h" | ||||
|  | ||||
| Preferences *Preferences::m_instance = 0; | ||||
|  | ||||
| @@ -463,6 +464,38 @@ void Preferences::setWebUiLocalAuthEnabled(bool enabled) | ||||
|     setValue("Preferences/WebUI/LocalHostAuth", enabled); | ||||
| } | ||||
|  | ||||
| bool Preferences::isWebUiAuthSubnetWhitelistEnabled() const | ||||
| { | ||||
|     return value("Preferences/WebUI/AuthSubnetWhitelistEnabled", false).toBool(); | ||||
| } | ||||
|  | ||||
| void Preferences::setWebUiAuthSubnetWhitelistEnabled(bool enabled) | ||||
| { | ||||
|     setValue("Preferences/WebUI/AuthSubnetWhitelistEnabled", enabled); | ||||
| } | ||||
|  | ||||
| QList<Utils::Net::Subnet> Preferences::getWebUiAuthSubnetWhitelist() const | ||||
| { | ||||
|     QList<Utils::Net::Subnet> subnets; | ||||
|     foreach (const QString &rawSubnet, value("Preferences/WebUI/AuthSubnetWhitelist").toStringList()) { | ||||
|         bool ok = false; | ||||
|         const Utils::Net::Subnet subnet = Utils::Net::parseSubnet(rawSubnet.trimmed(), &ok); | ||||
|         if (ok) | ||||
|             subnets.append(subnet); | ||||
|     } | ||||
|  | ||||
|     return subnets; | ||||
| } | ||||
|  | ||||
| void Preferences::setWebUiAuthSubnetWhitelist(const QList<Utils::Net::Subnet> &subnets) | ||||
| { | ||||
|     QStringList subnetsStringList; | ||||
|     for (const Utils::Net::Subnet &subnet : subnets) | ||||
|         subnetsStringList.append(Utils::Net::subnetToString(subnet)); | ||||
|  | ||||
|     setValue("Preferences/WebUI/AuthSubnetWhitelist", subnetsStringList); | ||||
| } | ||||
|  | ||||
| QString Preferences::getServerDomains() const | ||||
| { | ||||
|     return value("Preferences/WebUI/ServerDomains", "*").toString(); | ||||
|   | ||||
| @@ -33,15 +33,18 @@ | ||||
| #ifndef PREFERENCES_H | ||||
| #define PREFERENCES_H | ||||
|  | ||||
| #include <QTime> | ||||
| #include <QDateTime> | ||||
| #include <QHostAddress> | ||||
| #include <QList> | ||||
| #include <QSize> | ||||
| #include <QTimer> | ||||
| #include <QReadWriteLock> | ||||
| #include <QNetworkCookie> | ||||
| #include <QReadWriteLock> | ||||
| #include <QSize> | ||||
| #include <QStringList> | ||||
| #include <QTime> | ||||
| #include <QTimer> | ||||
| #include <QVariant> | ||||
|  | ||||
| #include "base/utils/net.h" | ||||
| #include "types.h" | ||||
|  | ||||
| enum scheduler_days | ||||
| @@ -170,10 +173,9 @@ public: | ||||
|     bool isSearchEnabled() const; | ||||
|     void setSearchEnabled(bool enabled); | ||||
|  | ||||
|     // HTTP Server | ||||
|     bool isWebUiEnabled() const; | ||||
|     void setWebUiEnabled(bool enabled); | ||||
|     bool isWebUiLocalAuthEnabled() const; | ||||
|     void setWebUiLocalAuthEnabled(bool enabled); | ||||
|     QString getServerDomains() const; | ||||
|     void setServerDomains(const QString &str); | ||||
|     QString getWebUiAddress() const; | ||||
| @@ -182,16 +184,28 @@ public: | ||||
|     void setWebUiPort(quint16 port); | ||||
|     bool useUPnPForWebUIPort() const; | ||||
|     void setUPnPForWebUIPort(bool enabled); | ||||
|  | ||||
|     // Authentication | ||||
|     bool isWebUiLocalAuthEnabled() const; | ||||
|     void setWebUiLocalAuthEnabled(bool enabled); | ||||
|     bool isWebUiAuthSubnetWhitelistEnabled() const; | ||||
|     void setWebUiAuthSubnetWhitelistEnabled(bool enabled); | ||||
|     QList<Utils::Net::Subnet> getWebUiAuthSubnetWhitelist() const; | ||||
|     void setWebUiAuthSubnetWhitelist(const QList<Utils::Net::Subnet> &subnets); | ||||
|     QString getWebUiUsername() const; | ||||
|     void setWebUiUsername(const QString &username); | ||||
|     QString getWebUiPassword() const; | ||||
|     void setWebUiPassword(const QString &new_password); | ||||
|  | ||||
|     // HTTPS | ||||
|     bool isWebUiHttpsEnabled() const; | ||||
|     void setWebUiHttpsEnabled(bool enabled); | ||||
|     QByteArray getWebUiHttpsCertificate() const; | ||||
|     void setWebUiHttpsCertificate(const QByteArray &data); | ||||
|     QByteArray getWebUiHttpsKey() const; | ||||
|     void setWebUiHttpsKey(const QByteArray &data); | ||||
|  | ||||
|     // Dynamic DNS | ||||
|     bool isDynDNSEnabled() const; | ||||
|     void setDynDNSEnabled(bool enabled); | ||||
|     DNS::Service getDynDNSService() const; | ||||
|   | ||||
| @@ -227,10 +227,9 @@ void Parser::parse(const QByteArray &feedData) | ||||
| // read and create items from a rss document | ||||
| void Parser::parse_impl(const QByteArray &feedData) | ||||
| { | ||||
|     qDebug() << Q_FUNC_INFO; | ||||
|  | ||||
|     QXmlStreamReader xml(feedData); | ||||
|     bool foundChannel = false; | ||||
|  | ||||
|     while (xml.readNextStartElement()) { | ||||
|         if (xml.name() == "rss") { | ||||
|             // Find channels | ||||
| @@ -258,11 +257,15 @@ void Parser::parse_impl(const QByteArray &feedData) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (xml.hasError()) | ||||
|         m_result.error = xml.errorString(); | ||||
|     else if (!foundChannel) | ||||
|     if (!foundChannel) { | ||||
|         m_result.error = tr("Invalid RSS feed."); | ||||
|     else | ||||
|     } | ||||
|     else if (xml.hasError()) { | ||||
|         m_result.error = tr("%1 (line: %2, column: %3, offset: %4).") | ||||
|                 .arg(xml.errorString()).arg(xml.lineNumber()) | ||||
|                 .arg(xml.columnNumber()).arg(xml.characterOffset()); | ||||
|     } | ||||
|     else { | ||||
|         // Sort article list chronologically | ||||
|         // NOTE: We don't need to sort it here if articles are always | ||||
|         // sorted in fetched XML in reverse chronological order | ||||
| @@ -271,6 +274,7 @@ void Parser::parse_impl(const QByteArray &feedData) | ||||
|         { | ||||
|             return a1["date"].toDateTime() < a2["date"].toDateTime(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     emit finished(m_result); | ||||
|     m_result.articles.clear(); // clear articles only | ||||
| @@ -288,35 +292,34 @@ void Parser::parseRssArticle(QXmlStreamReader &xml) | ||||
|             break; | ||||
|  | ||||
|         if (xml.isStartElement()) { | ||||
|             const QString text(xml.readElementText().trimmed()); | ||||
|  | ||||
|             if (name == QLatin1String("title")) { | ||||
|                 article[Article::KeyTitle] = text; | ||||
|                 article[Article::KeyTitle] = xml.readElementText().trimmed(); | ||||
|             } | ||||
|             else if (name == QLatin1String("enclosure")) { | ||||
|                 if (xml.attributes().value("type") == QLatin1String("application/x-bittorrent")) | ||||
|                     article[Article::KeyTorrentURL] = xml.attributes().value(QLatin1String("url")).toString(); | ||||
|             } | ||||
|             else if (name == QLatin1String("link")) { | ||||
|                 const QString text {xml.readElementText().trimmed()}; | ||||
|                 if (text.startsWith(QLatin1String("magnet:"), Qt::CaseInsensitive)) | ||||
|                     article[Article::KeyTorrentURL] = text; // magnet link instead of a news URL | ||||
|                 else | ||||
|                     article[Article::KeyLink] = text; | ||||
|             } | ||||
|             else if (name == QLatin1String("description")) { | ||||
|                 article[Article::KeyDescription] = text; | ||||
|                 article[Article::KeyDescription] = xml.readElementText(QXmlStreamReader::IncludeChildElements); | ||||
|             } | ||||
|             else if (name == QLatin1String("pubDate")) { | ||||
|                 article[Article::KeyDate] = parseDate(text); | ||||
|                 article[Article::KeyDate] = parseDate(xml.readElementText().trimmed()); | ||||
|             } | ||||
|             else if (name == QLatin1String("author")) { | ||||
|                 article[Article::KeyAuthor] = text; | ||||
|                 article[Article::KeyAuthor] = xml.readElementText().trimmed(); | ||||
|             } | ||||
|             else if (name == QLatin1String("guid")) { | ||||
|                 article[Article::KeyId] = text; | ||||
|                 article[Article::KeyId] = xml.readElementText().trimmed(); | ||||
|             } | ||||
|             else { | ||||
|                 article[name] = text; | ||||
|                 article[name] = xml.readElementText(QXmlStreamReader::IncludeChildElements); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -326,17 +329,14 @@ void Parser::parseRssArticle(QXmlStreamReader &xml) | ||||
|  | ||||
| void Parser::parseRSSChannel(QXmlStreamReader &xml) | ||||
| { | ||||
|     qDebug() << Q_FUNC_INFO; | ||||
|     Q_ASSERT(xml.isStartElement() && xml.name() == "channel"); | ||||
|  | ||||
|     while (!xml.atEnd()) { | ||||
|         xml.readNext(); | ||||
|  | ||||
|         if (xml.isStartElement()) { | ||||
|             if (xml.name() == "title") { | ||||
|             if (xml.name() == QLatin1String("title")) { | ||||
|                 m_result.title = xml.readElementText(); | ||||
|             } | ||||
|             else if (xml.name() == "lastBuildDate") { | ||||
|             else if (xml.name() == QLatin1String("lastBuildDate")) { | ||||
|                 QString lastBuildDate = xml.readElementText(); | ||||
|                 if (!lastBuildDate.isEmpty()) { | ||||
|                     if (m_result.lastBuildDate == lastBuildDate) { | ||||
| @@ -346,7 +346,7 @@ void Parser::parseRSSChannel(QXmlStreamReader &xml) | ||||
|                     m_result.lastBuildDate = lastBuildDate; | ||||
|                 } | ||||
|             } | ||||
|             else if (xml.name() == "item") { | ||||
|             else if (xml.name() == QLatin1String("item")) { | ||||
|                 parseRssArticle(xml); | ||||
|             } | ||||
|         } | ||||
| @@ -366,14 +366,12 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml) | ||||
|             break; | ||||
|  | ||||
|         if (xml.isStartElement()) { | ||||
|             const QString text(xml.readElementText().trimmed()); | ||||
|  | ||||
|             if (name == QLatin1String("title")) { | ||||
|                 article[Article::KeyTitle] = text; | ||||
|                 article[Article::KeyTitle] = xml.readElementText().trimmed(); | ||||
|             } | ||||
|             else if (name == QLatin1String("link")) { | ||||
|                 QString link = (xml.attributes().isEmpty() | ||||
|                                 ? text | ||||
|                                 ? xml.readElementText().trimmed() | ||||
|                                 : xml.attributes().value(QLatin1String("href")).toString()); | ||||
|  | ||||
|                 if (link.startsWith(QLatin1String("magnet:"), Qt::CaseInsensitive)) | ||||
| @@ -385,42 +383,38 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml) | ||||
|                     article[Article::KeyLink] = (m_baseUrl.isEmpty() ? link : m_baseUrl + link); | ||||
|  | ||||
|             } | ||||
|             else if ((name == QLatin1String("summary")) || (name == QLatin1String("content"))){ | ||||
|             else if ((name == QLatin1String("summary")) || (name == QLatin1String("content"))) { | ||||
|                 if (doubleContent) { // Duplicate content -> ignore | ||||
|                     xml.readNext(); | ||||
|  | ||||
|                     while ((xml.name() != QLatin1String("summary")) && (xml.name() != QLatin1String("content"))) | ||||
|                         xml.readNext(); | ||||
|  | ||||
|                     xml.skipCurrentElement(); | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 // Try to also parse broken articles, which don't use html '&' escapes | ||||
|                 // Actually works great for non-broken content too | ||||
|                 QString feedText = xml.readElementText(QXmlStreamReader::IncludeChildElements); | ||||
|                 if (!feedText.isEmpty()) | ||||
|                     article[Article::KeyDescription] = feedText.trimmed(); | ||||
|  | ||||
|                 QString feedText = xml.readElementText(QXmlStreamReader::IncludeChildElements).trimmed(); | ||||
|                 if (!feedText.isEmpty()) { | ||||
|                     article[Article::KeyDescription] = feedText; | ||||
|                     doubleContent = true; | ||||
|                 } | ||||
|             } | ||||
|             else if (name == QLatin1String("updated")) { | ||||
|                 // ATOM uses standard compliant date, don't do fancy stuff | ||||
|                 QDateTime articleDate = QDateTime::fromString(text, Qt::ISODate); | ||||
|                 QDateTime articleDate = QDateTime::fromString(xml.readElementText().trimmed(), Qt::ISODate); | ||||
|                 article[Article::KeyDate] = (articleDate.isValid() ? articleDate : QDateTime::currentDateTime()); | ||||
|             } | ||||
|             else if (name == QLatin1String("author")) { | ||||
|                 xml.readNext(); | ||||
|                 while (xml.name() != QLatin1String("author")) { | ||||
|                 while (xml.readNextStartElement()) { | ||||
|                     if (xml.name() == QLatin1String("name")) | ||||
|                         article[Article::KeyAuthor] = xml.readElementText().trimmed(); | ||||
|                     xml.readNext(); | ||||
|                     else | ||||
|                         xml.skipCurrentElement(); | ||||
|                 } | ||||
|             } | ||||
|             else if (name == QLatin1String("id")) { | ||||
|                 article[Article::KeyId] = text; | ||||
|                 article[Article::KeyId] = xml.readElementText().trimmed(); | ||||
|             } | ||||
|             else { | ||||
|                 article[name] = text; | ||||
|                 article[name] = xml.readElementText(QXmlStreamReader::IncludeChildElements); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -430,19 +424,16 @@ void Parser::parseAtomArticle(QXmlStreamReader &xml) | ||||
|  | ||||
| void Parser::parseAtomChannel(QXmlStreamReader &xml) | ||||
| { | ||||
|     qDebug() << Q_FUNC_INFO; | ||||
|     Q_ASSERT(xml.isStartElement() && xml.name() == "feed"); | ||||
|  | ||||
|     m_baseUrl = xml.attributes().value("xml:base").toString(); | ||||
|  | ||||
|     while (!xml.atEnd()) { | ||||
|         xml.readNext(); | ||||
|  | ||||
|         if (xml.isStartElement()) { | ||||
|             if (xml.name() == "title") { | ||||
|             if (xml.name() == QLatin1String("title")) { | ||||
|                 m_result.title = xml.readElementText(); | ||||
|             } | ||||
|             else if (xml.name() == "updated") { | ||||
|             else if (xml.name() == QLatin1String("updated")) { | ||||
|                 QString lastBuildDate = xml.readElementText(); | ||||
|                 if (!lastBuildDate.isEmpty()) { | ||||
|                     if (m_result.lastBuildDate == lastBuildDate) { | ||||
| @@ -452,7 +443,7 @@ void Parser::parseAtomChannel(QXmlStreamReader &xml) | ||||
|                     m_result.lastBuildDate = lastBuildDate; | ||||
|                 } | ||||
|             } | ||||
|             else if (xml.name() == "entry") { | ||||
|             else if (xml.name() == QLatin1String("entry")) { | ||||
|                 parseAtomArticle(xml); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
|  | ||||
| #include "rss_autodownloader.h" | ||||
|  | ||||
| #include <QDataStream> | ||||
| #include <QDebug> | ||||
| #include <QJsonArray> | ||||
| #include <QJsonDocument> | ||||
| @@ -37,6 +38,7 @@ | ||||
| #include <QThread> | ||||
| #include <QTimer> | ||||
| #include <QVariant> | ||||
| #include <QVector> | ||||
|  | ||||
| #include "../bittorrent/magneturi.h" | ||||
| #include "../bittorrent/session.h" | ||||
| @@ -63,6 +65,32 @@ const QString RulesFileName(QStringLiteral("download_rules.json")); | ||||
|  | ||||
| const QString SettingsKey_ProcessingEnabled(QStringLiteral("RSS/AutoDownloader/EnableProcessing")); | ||||
|  | ||||
| namespace | ||||
| { | ||||
|     QVector<RSS::AutoDownloadRule> rulesFromJSON(const QByteArray &jsonData) | ||||
|     { | ||||
|         QJsonParseError jsonError; | ||||
|         QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &jsonError); | ||||
|         if (jsonError.error != QJsonParseError::NoError) | ||||
|             throw RSS::ParsingError(jsonError.errorString()); | ||||
|  | ||||
|         if (!jsonDoc.isObject()) | ||||
|             throw RSS::ParsingError(RSS::AutoDownloader::tr("Invalid data format.")); | ||||
|  | ||||
|         const QJsonObject jsonObj {jsonDoc.object()}; | ||||
|         QVector<RSS::AutoDownloadRule> rules; | ||||
|         for (auto it = jsonObj.begin(); it != jsonObj.end(); ++it) { | ||||
|             const QJsonValue jsonVal {it.value()}; | ||||
|             if (!jsonVal.isObject()) | ||||
|                 throw RSS::ParsingError(RSS::AutoDownloader::tr("Invalid data format.")); | ||||
|  | ||||
|             rules.append(RSS::AutoDownloadRule::fromJsonObject(jsonVal.toObject(), it.key())); | ||||
|         } | ||||
|  | ||||
|         return rules; | ||||
|     } | ||||
| } | ||||
|  | ||||
| using namespace RSS; | ||||
|  | ||||
| QPointer<AutoDownloader> AutoDownloader::m_instance = nullptr; | ||||
| @@ -84,8 +112,8 @@ AutoDownloader::AutoDownloader() | ||||
|     connect(m_ioThread, &QThread::finished, m_fileStorage, &AsyncFileStorage::deleteLater); | ||||
|     connect(m_fileStorage, &AsyncFileStorage::failed, [](const QString &fileName, const QString &errorString) | ||||
|     { | ||||
|         Logger::instance()->addMessage(QString("Couldn't save RSS AutoDownloader data in %1. Error: %2") | ||||
|                                        .arg(fileName).arg(errorString), Log::WARNING); | ||||
|         LogMsg(tr("Couldn't save RSS AutoDownloader data in %1. Error: %2") | ||||
|                .arg(fileName).arg(errorString), Log::CRITICAL); | ||||
|     }); | ||||
|  | ||||
|     m_ioThread->start(); | ||||
| @@ -174,6 +202,70 @@ void AutoDownloader::removeRule(const QString &ruleName) | ||||
|     } | ||||
| } | ||||
|  | ||||
| QByteArray AutoDownloader::exportRules(AutoDownloader::RulesFileFormat format) const | ||||
| { | ||||
|     switch (format) { | ||||
|     case RulesFileFormat::Legacy: | ||||
|         return exportRulesToLegacyFormat(); | ||||
|     default: | ||||
|         return exportRulesToJSONFormat(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void AutoDownloader::importRules(const QByteArray &data, AutoDownloader::RulesFileFormat format) | ||||
| { | ||||
|     switch (format) { | ||||
|     case RulesFileFormat::Legacy: | ||||
|         importRulesFromLegacyFormat(data); | ||||
|         break; | ||||
|     default: | ||||
|         importRulesFromJSONFormat(data); | ||||
|     } | ||||
| } | ||||
|  | ||||
| QByteArray AutoDownloader::exportRulesToJSONFormat() const | ||||
| { | ||||
|     QJsonObject jsonObj; | ||||
|     for (const auto &rule : rules()) | ||||
|         jsonObj.insert(rule.name(), rule.toJsonObject()); | ||||
|  | ||||
|     return QJsonDocument(jsonObj).toJson(); | ||||
| } | ||||
|  | ||||
| void AutoDownloader::importRulesFromJSONFormat(const QByteArray &data) | ||||
| { | ||||
|     const auto rules = rulesFromJSON(data); | ||||
|     for (const auto &rule : rules) | ||||
|         insertRule(rule); | ||||
| } | ||||
|  | ||||
| QByteArray AutoDownloader::exportRulesToLegacyFormat() const | ||||
| { | ||||
|     QVariantHash dict; | ||||
|     for (const auto &rule : rules()) | ||||
|         dict[rule.name()] = rule.toLegacyDict(); | ||||
|  | ||||
|     QByteArray data; | ||||
|     QDataStream out(&data, QIODevice::WriteOnly); | ||||
|     out.setVersion(QDataStream::Qt_4_5); | ||||
|     out << dict; | ||||
|  | ||||
|     return data; | ||||
| } | ||||
|  | ||||
| void AutoDownloader::importRulesFromLegacyFormat(const QByteArray &data) | ||||
| { | ||||
|     QDataStream in(data); | ||||
|     in.setVersion(QDataStream::Qt_4_5); | ||||
|     QVariantHash dict; | ||||
|     in >> dict; | ||||
|     if (in.status() != QDataStream::Ok) | ||||
|         throw ParsingError(tr("Invalid data format")); | ||||
|  | ||||
|     for (const QVariant &val : dict) | ||||
|         insertRule(AutoDownloadRule::fromLegacyDict(val.toHash())); | ||||
| } | ||||
|  | ||||
| void AutoDownloader::process() | ||||
| { | ||||
|     if (m_processingQueue.isEmpty()) return; // processing was disabled | ||||
| @@ -276,39 +368,20 @@ void AutoDownloader::load() | ||||
|     else if (rulesFile.open(QFile::ReadOnly)) | ||||
|         loadRules(rulesFile.readAll()); | ||||
|     else | ||||
|         Logger::instance()->addMessage( | ||||
|                     QString("Couldn't read RSS AutoDownloader rules from %1. Error: %2") | ||||
|                     .arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::WARNING); | ||||
|         LogMsg(tr("Couldn't read RSS AutoDownloader rules from %1. Error: %2") | ||||
|                .arg(rulesFile.fileName()).arg(rulesFile.errorString()), Log::CRITICAL); | ||||
| } | ||||
|  | ||||
| void AutoDownloader::loadRules(const QByteArray &data) | ||||
| { | ||||
|     QJsonParseError jsonError; | ||||
|     QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); | ||||
|     if (jsonError.error != QJsonParseError::NoError) { | ||||
|         Logger::instance()->addMessage( | ||||
|                     QString("Couldn't parse RSS AutoDownloader rules. Error: %1") | ||||
|                     .arg(jsonError.errorString()), Log::WARNING); | ||||
|         return; | ||||
|     try { | ||||
|         const auto rules = rulesFromJSON(data); | ||||
|         for (const auto &rule : rules) | ||||
|             setRule_impl(rule); | ||||
|     } | ||||
|  | ||||
|     if (!jsonDoc.isObject()) { | ||||
|         Logger::instance()->addMessage( | ||||
|                     QString("Couldn't load RSS AutoDownloader rules. Invalid data format."), Log::WARNING); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     QJsonObject jsonObj = jsonDoc.object(); | ||||
|     foreach (const QString &key, jsonObj.keys()) { | ||||
|         const QJsonValue jsonVal = jsonObj.value(key); | ||||
|         if (!jsonVal.isObject()) { | ||||
|             Logger::instance()->addMessage( | ||||
|                         QString("Couldn't load RSS AutoDownloader rule '%1'. Invalid data format.") | ||||
|                         .arg(key), Log::WARNING); | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         setRule_impl(AutoDownloadRule::fromJsonObject(jsonVal.toObject(), key)); | ||||
|     catch (const ParsingError &error) { | ||||
|         LogMsg(tr("Couldn't load RSS AutoDownloader rules. Reason: %1") | ||||
|                .arg(error.message()), Log::CRITICAL); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -317,7 +390,7 @@ void AutoDownloader::loadRulesLegacy() | ||||
|     SettingsPtr settings = Profile::instance().applicationSettings(QStringLiteral("qBittorrent-rss")); | ||||
|     QVariantHash rules = settings->value(QStringLiteral("download_rules")).toHash(); | ||||
|     foreach (const QVariant &ruleVar, rules) { | ||||
|         auto rule = AutoDownloadRule::fromVariantHash(ruleVar.toHash()); | ||||
|         auto rule = AutoDownloadRule::fromLegacyDict(ruleVar.toHash()); | ||||
|         if (!rule.name().isEmpty()) | ||||
|             insertRule(rule); | ||||
|     } | ||||
| @@ -385,3 +458,13 @@ void AutoDownloader::timerEvent(QTimerEvent *event) | ||||
|     Q_UNUSED(event); | ||||
|     store(); | ||||
| } | ||||
|  | ||||
| ParsingError::ParsingError(const QString &message) | ||||
|     : std::runtime_error(message.toUtf8().data()) | ||||
| { | ||||
| } | ||||
|  | ||||
| QString ParsingError::message() const | ||||
| { | ||||
|     return what(); | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,8 @@ | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <stdexcept> | ||||
|  | ||||
| #include <QBasicTimer> | ||||
| #include <QHash> | ||||
| #include <QList> | ||||
| @@ -49,6 +51,13 @@ namespace RSS | ||||
|  | ||||
|     class AutoDownloadRule; | ||||
|  | ||||
|     class ParsingError : public std::runtime_error | ||||
|     { | ||||
|     public: | ||||
|         explicit ParsingError(const QString &message); | ||||
|         QString message() const; | ||||
|     }; | ||||
|  | ||||
|     class AutoDownloader final: public QObject | ||||
|     { | ||||
|         Q_OBJECT | ||||
| @@ -60,6 +69,12 @@ namespace RSS | ||||
|         ~AutoDownloader() override; | ||||
|  | ||||
|     public: | ||||
|         enum class RulesFileFormat | ||||
|         { | ||||
|             Legacy, | ||||
|             JSON | ||||
|         }; | ||||
|  | ||||
|         static AutoDownloader *instance(); | ||||
|  | ||||
|         bool isProcessingEnabled() const; | ||||
| @@ -73,6 +88,9 @@ namespace RSS | ||||
|         bool renameRule(const QString &ruleName, const QString &newRuleName); | ||||
|         void removeRule(const QString &ruleName); | ||||
|  | ||||
|         QByteArray exportRules(RulesFileFormat format = RulesFileFormat::JSON) const; | ||||
|         void importRules(const QByteArray &data, RulesFileFormat format = RulesFileFormat::JSON); | ||||
|  | ||||
|     signals: | ||||
|         void processingStateChanged(bool enabled); | ||||
|         void ruleAdded(const QString &ruleName); | ||||
| @@ -98,6 +116,10 @@ namespace RSS | ||||
|         void loadRulesLegacy(); | ||||
|         void store(); | ||||
|         void storeDeferred(); | ||||
|         QByteArray exportRulesToJSONFormat() const; | ||||
|         void importRulesFromJSONFormat(const QByteArray &data); | ||||
|         QByteArray exportRulesToLegacyFormat() const; | ||||
|         void importRulesFromLegacyFormat(const QByteArray &data); | ||||
|  | ||||
|         static QPointer<AutoDownloader> m_instance; | ||||
|  | ||||
|   | ||||
| @@ -63,11 +63,29 @@ namespace | ||||
|     QJsonValue triStateBoolToJsonValue(const TriStateBool &triStateBool) | ||||
|     { | ||||
|         switch (static_cast<int>(triStateBool)) { | ||||
|         case 0:  return false; break; | ||||
|         case 1:  return true; break; | ||||
|         case 0:  return false; | ||||
|         case 1:  return true; | ||||
|         default: return QJsonValue(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     TriStateBool addPausedLegacyToTriStateBool(int val) | ||||
|     { | ||||
|         switch (val) { | ||||
|         case 1:  return TriStateBool::True; // always | ||||
|         case 2:  return TriStateBool::False; // never | ||||
|         default: return TriStateBool::Undefined; // default | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     int triStateBoolToAddPausedLegacy(const TriStateBool &triStateBool) | ||||
|     { | ||||
|         switch (static_cast<int>(triStateBool)) { | ||||
|         case 0:  return 2; // never | ||||
|         case 1:  return 1; // always | ||||
|         default: return 0; // default | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| const QString Str_Name(QStringLiteral("name")); | ||||
| @@ -378,21 +396,37 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co | ||||
|     return rule; | ||||
| } | ||||
|  | ||||
| AutoDownloadRule AutoDownloadRule::fromVariantHash(const QVariantHash &varHash) | ||||
| QVariantHash AutoDownloadRule::toLegacyDict() const | ||||
| { | ||||
|     AutoDownloadRule rule(varHash.value("name").toString()); | ||||
|     return {{"name", name()}, | ||||
|         {"must_contain", mustContain()}, | ||||
|         {"must_not_contain", mustNotContain()}, | ||||
|         {"save_path", savePath()}, | ||||
|         {"affected_feeds", feedURLs()}, | ||||
|         {"enabled", isEnabled()}, | ||||
|         {"category_assigned", assignedCategory()}, | ||||
|         {"use_regex", useRegex()}, | ||||
|         {"add_paused", triStateBoolToAddPausedLegacy(addPaused())}, | ||||
|         {"episode_filter", episodeFilter()}, | ||||
|         {"last_match", lastMatch()}, | ||||
|         {"ignore_days", ignoreDays()}}; | ||||
| } | ||||
|  | ||||
|     rule.setUseRegex(varHash.value("use_regex", false).toBool()); | ||||
|     rule.setMustContain(varHash.value("must_contain").toString()); | ||||
|     rule.setMustNotContain(varHash.value("must_not_contain").toString()); | ||||
|     rule.setEpisodeFilter(varHash.value("episode_filter").toString()); | ||||
|     rule.setFeedURLs(varHash.value("affected_feeds").toStringList()); | ||||
|     rule.setEnabled(varHash.value("enabled", false).toBool()); | ||||
|     rule.setSavePath(varHash.value("save_path").toString()); | ||||
|     rule.setCategory(varHash.value("category_assigned").toString()); | ||||
|     rule.setAddPaused(TriStateBool(varHash.value("add_paused").toInt() - 1)); | ||||
|     rule.setLastMatch(varHash.value("last_match").toDateTime()); | ||||
|     rule.setIgnoreDays(varHash.value("ignore_days").toInt()); | ||||
| AutoDownloadRule AutoDownloadRule::fromLegacyDict(const QVariantHash &dict) | ||||
| { | ||||
|     AutoDownloadRule rule(dict.value("name").toString()); | ||||
|  | ||||
|     rule.setUseRegex(dict.value("use_regex", false).toBool()); | ||||
|     rule.setMustContain(dict.value("must_contain").toString()); | ||||
|     rule.setMustNotContain(dict.value("must_not_contain").toString()); | ||||
|     rule.setEpisodeFilter(dict.value("episode_filter").toString()); | ||||
|     rule.setFeedURLs(dict.value("affected_feeds").toStringList()); | ||||
|     rule.setEnabled(dict.value("enabled", false).toBool()); | ||||
|     rule.setSavePath(dict.value("save_path").toString()); | ||||
|     rule.setCategory(dict.value("category_assigned").toString()); | ||||
|     rule.setAddPaused(addPausedLegacyToTriStateBool(dict.value("add_paused").toInt())); | ||||
|     rule.setLastMatch(dict.value("last_match").toDateTime()); | ||||
|     rule.setIgnoreDays(dict.value("ignore_days").toInt()); | ||||
|  | ||||
|     return rule; | ||||
| } | ||||
|   | ||||
| @@ -84,7 +84,9 @@ namespace RSS | ||||
|  | ||||
|         QJsonObject toJsonObject() const; | ||||
|         static AutoDownloadRule fromJsonObject(const QJsonObject &jsonObj, const QString &name = ""); | ||||
|         static AutoDownloadRule fromVariantHash(const QVariantHash &varHash); | ||||
|  | ||||
|         QVariantHash toLegacyDict() const; | ||||
|         static AutoDownloadRule fromLegacyDict(const QVariantHash &dict); | ||||
|  | ||||
|     private: | ||||
|         bool matches(const QString &articleTitle, const QString &expression) const; | ||||
|   | ||||
| @@ -31,6 +31,7 @@ | ||||
| #include "rss_feed.h" | ||||
|  | ||||
| #include <QCryptographicHash> | ||||
| #include <QDebug> | ||||
| #include <QDir> | ||||
| #include <QJsonArray> | ||||
| #include <QJsonDocument> | ||||
|   | ||||
| @@ -163,7 +163,7 @@ bool Session::moveItem(const QString &itemPath, const QString &destPath, QString | ||||
|     auto item = m_itemsByPath.value(itemPath); | ||||
|     if (!item) { | ||||
|         if (error) | ||||
|             *error = tr("Item doesn't exists: %1.").arg(itemPath); | ||||
|             *error = tr("Item doesn't exist: %1.").arg(itemPath); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
| @@ -201,7 +201,7 @@ bool Session::removeItem(const QString &itemPath, QString *error) | ||||
|     auto item = m_itemsByPath.value(itemPath); | ||||
|     if (!item) { | ||||
|         if (error) | ||||
|             *error = tr("Item doesn't exists: %1.").arg(itemPath); | ||||
|             *error = tr("Item doesn't exist: %1.").arg(itemPath); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,7 @@ | ||||
| #include "net.h" | ||||
| #include <QHostAddress> | ||||
| #include <QString> | ||||
| #include <QStringList> | ||||
|  | ||||
| namespace Utils | ||||
| { | ||||
| @@ -38,5 +39,55 @@ namespace Utils | ||||
|         { | ||||
|             return !QHostAddress(ip).isNull(); | ||||
|         } | ||||
|  | ||||
|         Subnet parseSubnet(const QString &subnetStr, bool *ok) | ||||
|         { | ||||
|             const Subnet invalid = qMakePair(QHostAddress(), -1); | ||||
|             const Subnet subnet = QHostAddress::parseSubnet(subnetStr); | ||||
|             if (ok) | ||||
|                 *ok = (subnet != invalid); | ||||
|             return subnet; | ||||
|         } | ||||
|  | ||||
|         bool canParseSubnet(const QString &subnetStr) | ||||
|         { | ||||
|             bool ok = false; | ||||
|             parseSubnet(subnetStr, &ok); | ||||
|             return ok; | ||||
|         } | ||||
|  | ||||
|         bool isLoopbackAddress(const QHostAddress &addr) | ||||
|         { | ||||
|             return (addr == QHostAddress::LocalHost) | ||||
|                     || (addr == QHostAddress::LocalHostIPv6) | ||||
|                     || (addr == QHostAddress(QLatin1String("::ffff:127.0.0.1"))); | ||||
|         } | ||||
|  | ||||
|         bool isIPInRange(const QHostAddress &addr, const QList<Subnet> &subnets) | ||||
|         { | ||||
|             QHostAddress protocolEquivalentAddress; | ||||
|             bool addrConversionOk = false; | ||||
|  | ||||
|             if (addr.protocol() == QAbstractSocket::IPv4Protocol) { | ||||
|                 // always succeeds | ||||
|                 protocolEquivalentAddress = QHostAddress(addr.toIPv6Address()); | ||||
|                 addrConversionOk = true; | ||||
|             } | ||||
|             else { | ||||
|                 // only succeeds when addr is an ipv4-mapped ipv6 address | ||||
|                 protocolEquivalentAddress = QHostAddress(addr.toIPv4Address(&addrConversionOk)); | ||||
|             } | ||||
|  | ||||
|             for (const Subnet &subnet : subnets) | ||||
|                 if (addr.isInSubnet(subnet) || (addrConversionOk && protocolEquivalentAddress.isInSubnet(subnet))) | ||||
|                     return true; | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         QString subnetToString(const Subnet &subnet) | ||||
|         { | ||||
|             return subnet.first.toString() + '/' + QString::number(subnet.second); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,13 +28,26 @@ | ||||
|  | ||||
| #ifndef BASE_UTILS_NET_H | ||||
| #define BASE_UTILS_NET_H | ||||
|  | ||||
| #include <QList> | ||||
| #include <QPair> | ||||
|  | ||||
| class QHostAddress; | ||||
| class QString; | ||||
| class QStringList; | ||||
|  | ||||
| namespace Utils | ||||
| { | ||||
|     namespace Net | ||||
|     { | ||||
|         using Subnet = QPair<QHostAddress, int>; | ||||
|  | ||||
|         bool isValidIP(const QString &ip); | ||||
|         Subnet parseSubnet(const QString &subnetStr, bool *ok = nullptr); | ||||
|         bool canParseSubnet(const QString &subnetStr); | ||||
|         bool isLoopbackAddress(const QHostAddress &addr); | ||||
|         bool isIPInRange(const QHostAddress &addr, const QList<Subnet> &subnets); | ||||
|         QString subnetToString(const Subnet &subnet); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -33,9 +33,9 @@ | ||||
|  | ||||
| #include <QByteArray> | ||||
| #include <QCollator> | ||||
| #include <QtGlobal> | ||||
| #include <QLocale> | ||||
| #include <QRegExp> | ||||
| #include <QtGlobal> | ||||
| #ifdef Q_OS_MAC | ||||
| #include <QThreadStorage> | ||||
| #endif | ||||
| @@ -45,110 +45,103 @@ namespace | ||||
|     class NaturalCompare | ||||
|     { | ||||
|     public: | ||||
|         explicit NaturalCompare(const bool caseSensitive = true) | ||||
|             : m_caseSensitive(caseSensitive) | ||||
|         explicit NaturalCompare(const Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive) | ||||
|             : m_caseSensitivity(caseSensitivity) | ||||
|         { | ||||
| #if defined(Q_OS_WIN) | ||||
| #ifdef Q_OS_WIN | ||||
|             // Without ICU library, QCollator uses the native API on Windows 7+. But that API | ||||
|             // sorts older versions of μTorrent differently than the newer ones because the | ||||
|             // 'μ' character is encoded differently and the native API can't cope with that. | ||||
|             // So default to using our custom natural sorting algorithm instead. | ||||
|             // See #5238 and #5240 | ||||
|             // Without ICU library, QCollator doesn't support `setNumericMode(true)` on OS older than Win7 | ||||
|             // if (QSysInfo::windowsVersion() < QSysInfo::WV_WINDOWS7) | ||||
|                 return; | ||||
| #endif | ||||
|             // Without ICU library, QCollator doesn't support `setNumericMode(true)` on an OS older than Win7 | ||||
| #else | ||||
|             m_collator.setNumericMode(true); | ||||
|             m_collator.setCaseSensitivity(caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive); | ||||
|         } | ||||
|  | ||||
|         bool operator()(const QString &left, const QString &right) const | ||||
|         { | ||||
| #if defined(Q_OS_WIN) | ||||
|             // Without ICU library, QCollator uses the native API on Windows 7+. But that API | ||||
|             // sorts older versions of μTorrent differently than the newer ones because the | ||||
|             // 'μ' character is encoded differently and the native API can't cope with that. | ||||
|             // So default to using our custom natural sorting algorithm instead. | ||||
|             // See #5238 and #5240 | ||||
|             // Without ICU library, QCollator doesn't support `setNumericMode(true)` on OS older than Win7 | ||||
|             // if (QSysInfo::windowsVersion() < QSysInfo::WV_WINDOWS7) | ||||
|                 return lessThan(left, right); | ||||
|             m_collator.setCaseSensitivity(caseSensitivity); | ||||
| #endif | ||||
|             return (m_collator.compare(left, right) < 0); | ||||
|         } | ||||
|  | ||||
|         bool lessThan(const QString &left, const QString &right) const | ||||
|         int operator()(const QString &left, const QString &right) const | ||||
|         { | ||||
|             // Return value `false` indicates `right` should go before `left`, otherwise, after | ||||
|             int posL = 0; | ||||
|             int posR = 0; | ||||
|             while (true) { | ||||
|                 while (true) { | ||||
|                     if ((posL == left.size()) || (posR == right.size())) | ||||
|                         return (left.size() < right.size());  // when a shorter string is another string's prefix, shorter string place before longer string | ||||
|  | ||||
|                     QChar leftChar = m_caseSensitive ? left[posL] : left[posL].toLower(); | ||||
|                     QChar rightChar = m_caseSensitive ? right[posR] : right[posR].toLower(); | ||||
|                     if (leftChar == rightChar) | ||||
|                         ;  // compare next character | ||||
|                     else if (leftChar.isDigit() && rightChar.isDigit()) | ||||
|                         break; // Both are digits, break this loop and compare numbers | ||||
|                     else | ||||
|                         return leftChar < rightChar; | ||||
|  | ||||
|                     ++posL; | ||||
|                     ++posR; | ||||
|                 } | ||||
|  | ||||
|                 int startL = posL; | ||||
|                 while ((posL < left.size()) && left[posL].isDigit()) | ||||
|                     ++posL; | ||||
|                 int numL = left.midRef(startL, posL - startL).toInt(); | ||||
|  | ||||
|                 int startR = posR; | ||||
|                 while ((posR < right.size()) && right[posR].isDigit()) | ||||
|                     ++posR; | ||||
|                 int numR = right.midRef(startR, posR - startR).toInt(); | ||||
|  | ||||
|                 if (numL != numR) | ||||
|                     return (numL < numR); | ||||
|  | ||||
|                 // Strings + digits do match and we haven't hit string end | ||||
|                 // Do another round | ||||
|             } | ||||
|             return false; | ||||
| #ifdef Q_OS_WIN | ||||
|             return compare(left, right); | ||||
| #else | ||||
|             return m_collator.compare(left, right); | ||||
| #endif | ||||
|         } | ||||
|  | ||||
|     private: | ||||
|         int compare(const QString &left, const QString &right) const | ||||
|         { | ||||
|             // Return value <0: `left` is smaller than `right` | ||||
|             // Return value >0: `left` is greater than `right` | ||||
|             // Return value =0: both strings are equal | ||||
|  | ||||
|             int posL = 0; | ||||
|             int posR = 0; | ||||
|             while (true) { | ||||
|                 if ((posL == left.size()) || (posR == right.size())) | ||||
|                     return (left.size() - right.size());  // when a shorter string is another string's prefix, shorter string place before longer string | ||||
|  | ||||
|                 const QChar leftChar = (m_caseSensitivity == Qt::CaseSensitive) ? left[posL] : left[posL].toLower(); | ||||
|                 const QChar rightChar = (m_caseSensitivity == Qt::CaseSensitive) ? right[posR] : right[posR].toLower(); | ||||
|                 if (leftChar == rightChar) { | ||||
|                     // compare next character | ||||
|                     ++posL; | ||||
|                     ++posR; | ||||
|                 } | ||||
|                 else if (leftChar.isDigit() && rightChar.isDigit()) { | ||||
|                     // Both are digits, compare the numbers | ||||
|                     const auto consumeNumber = [](const QString &str, int &pos) -> int | ||||
|                     { | ||||
|                         const int start = pos; | ||||
|                         while ((pos < str.size()) && str[pos].isDigit()) | ||||
|                             ++pos; | ||||
|                         return str.midRef(start, (pos - start)).toInt(); | ||||
|                     }; | ||||
|  | ||||
|                     const int numL = consumeNumber(left, posL); | ||||
|                     const int numR = consumeNumber(right, posR); | ||||
|                     if (numL != numR) | ||||
|                         return (numL - numR); | ||||
|  | ||||
|                     // String + digits do match and we haven't hit the end of both strings | ||||
|                     // then continue to consume the remainings | ||||
|                 } | ||||
|                 else { | ||||
|                     return (leftChar.unicode() - rightChar.unicode()); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         QCollator m_collator; | ||||
|         const bool m_caseSensitive; | ||||
|         const Qt::CaseSensitivity m_caseSensitivity; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| bool Utils::String::naturalCompareCaseSensitive(const QString &left, const QString &right) | ||||
| int Utils::String::naturalCompare(const QString &left, const QString &right, const Qt::CaseSensitivity caseSensitivity) | ||||
| { | ||||
|     // provide a single `NaturalCompare` instance for easy use | ||||
|     // https://doc.qt.io/qt-5/threads-reentrancy.html | ||||
|     if (caseSensitivity == Qt::CaseSensitive) { | ||||
| #ifdef Q_OS_MAC  // workaround for Apple xcode: https://stackoverflow.com/a/29929949 | ||||
|         static QThreadStorage<NaturalCompare> nCmp; | ||||
|     if (!nCmp.hasLocalData()) nCmp.setLocalData(NaturalCompare(true)); | ||||
|         if (!nCmp.hasLocalData()) | ||||
|             nCmp.setLocalData(NaturalCompare(Qt::CaseSensitive)); | ||||
|         return (nCmp.localData())(left, right); | ||||
| #else | ||||
|     thread_local NaturalCompare nCmp(true); | ||||
|         thread_local NaturalCompare nCmp(Qt::CaseSensitive); | ||||
|         return nCmp(left, right); | ||||
| #endif | ||||
| } | ||||
|     } | ||||
|  | ||||
| bool Utils::String::naturalCompareCaseInsensitive(const QString &left, const QString &right) | ||||
| { | ||||
|     // provide a single `NaturalCompare` instance for easy use | ||||
|     // https://doc.qt.io/qt-5/threads-reentrancy.html | ||||
| #ifdef Q_OS_MAC  // workaround for Apple xcode: https://stackoverflow.com/a/29929949 | ||||
| #ifdef Q_OS_MAC | ||||
|     static QThreadStorage<NaturalCompare> nCmp; | ||||
|     if (!nCmp.hasLocalData()) nCmp.setLocalData(NaturalCompare(false)); | ||||
|     if (!nCmp.hasLocalData()) | ||||
|         nCmp.setLocalData(NaturalCompare(Qt::CaseInsensitive)); | ||||
|     return (nCmp.localData())(left, right); | ||||
| #else | ||||
|     thread_local NaturalCompare nCmp(false); | ||||
|     thread_local NaturalCompare nCmp(Qt::CaseInsensitive); | ||||
|     return nCmp(left, right); | ||||
| #endif | ||||
| } | ||||
| @@ -188,4 +181,3 @@ QString Utils::String::wildcardToRegex(const QString &pattern) | ||||
| { | ||||
|     return qt_regexp_toCanonical(pattern, QRegExp::Wildcard); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -45,8 +45,12 @@ namespace Utils | ||||
|         // Taken from https://crackstation.net/hashing-security.htm | ||||
|         bool slowEquals(const QByteArray &a, const QByteArray &b); | ||||
|  | ||||
|         bool naturalCompareCaseSensitive(const QString &left, const QString &right); | ||||
|         bool naturalCompareCaseInsensitive(const QString &left, const QString &right); | ||||
|         int naturalCompare(const QString &left, const QString &right, const Qt::CaseSensitivity caseSensitivity); | ||||
|         template <Qt::CaseSensitivity caseSensitivity> | ||||
|         bool naturalLessThan(const QString &left, const QString &right) | ||||
|         { | ||||
|             return (naturalCompare(left, right, caseSensitivity) < 0); | ||||
|         } | ||||
|  | ||||
|         QString wildcardToRegex(const QString &pattern); | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,6 @@ set(QBT_GUI_HEADERS | ||||
| about_imp.h | ||||
| addnewtorrentdialog.h | ||||
| advancedsettings.h | ||||
| advancedsettings.h | ||||
| autoexpandabledialog.h | ||||
| banlistoptions.h | ||||
| categoryfiltermodel.h | ||||
| @@ -45,6 +44,7 @@ fspathedit.h | ||||
| fspathedit_p.h | ||||
| guiiconprovider.h | ||||
| hidabletabwidget.h | ||||
| ipsubnetwhitelistoptionsdialog.h | ||||
| loglistwidget.h | ||||
| mainwindow.h | ||||
| messageboxraised.h | ||||
| @@ -90,6 +90,7 @@ executionlog.cpp | ||||
| fspathedit.cpp | ||||
| fspathedit_p.cpp | ||||
| guiiconprovider.cpp | ||||
| ipsubnetwhitelistoptionsdialog.cpp | ||||
| loglistwidget.cpp | ||||
| mainwindow.cpp | ||||
| messageboxraised.cpp | ||||
| @@ -135,6 +136,7 @@ mainwindow.ui | ||||
| about.ui | ||||
| banlistoptions.ui | ||||
| cookiesdialog.ui | ||||
| ipsubnetwhitelistoptionsdialog.ui | ||||
| previewselectdialog.ui | ||||
| login.ui | ||||
| downloadfromurldlg.ui | ||||
|   | ||||
| @@ -128,7 +128,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inP | ||||
|  | ||||
|     // Load categories | ||||
|     QStringList categories = session->categories().keys(); | ||||
|     std::sort(categories.begin(), categories.end(), Utils::String::naturalCompareCaseInsensitive); | ||||
|     std::sort(categories.begin(), categories.end(), Utils::String::naturalLessThan<Qt::CaseInsensitive>); | ||||
|     QString defaultCategory = settings()->loadValue(KEY_DEFAULTCATEGORY).toString(); | ||||
|  | ||||
|     if (!m_torrentParams.category.isEmpty()) | ||||
|   | ||||
| @@ -70,8 +70,11 @@ void BanListOptions::on_buttonBox_accepted() | ||||
|             IPList << index.data().toString(); | ||||
|         } | ||||
|         BitTorrent::Session::instance()->setBannedIPs(IPList); | ||||
|     } | ||||
|         QDialog::accept(); | ||||
|     } | ||||
|     else { | ||||
|         QDialog::reject(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void BanListOptions::on_buttonBanIP_clicked() | ||||
|   | ||||
| @@ -50,8 +50,12 @@ bool CategoryFilterProxyModel::lessThan(const QModelIndex &left, const QModelInd | ||||
| { | ||||
|     // "All" and "Uncategorized" must be left in place | ||||
|     if (CategoryFilterModel::isSpecialItem(left) || CategoryFilterModel::isSpecialItem(right)) | ||||
|         return left.row() < right.row(); | ||||
|     else | ||||
|         return Utils::String::naturalCompareCaseInsensitive( | ||||
|                 left.data().toString(), right.data().toString()); | ||||
|         return (left < right); | ||||
|  | ||||
|     int result = Utils::String::naturalCompare(left.data().toString(), right.data().toString() | ||||
|         , Qt::CaseInsensitive); | ||||
|     if (result != 0) | ||||
|         return (result < 0); | ||||
|  | ||||
|     return (mapFromSource(left) < mapFromSource(right)); | ||||
| } | ||||
|   | ||||
| @@ -60,7 +60,7 @@ class downloadFromURL : public QDialog, private Ui::downloadFromURL | ||||
|  | ||||
|       // Paste clipboard if there is an URL in it | ||||
|       QString clip_txt = qApp->clipboard()->text(); | ||||
|       QStringList clip_txt_list = clip_txt.split(QString::fromUtf8("\n")); | ||||
|       QStringList clip_txt_list = clip_txt.split(QLatin1Char('\n')); | ||||
|       clip_txt.clear(); | ||||
|       QStringList clip_txt_list_cleaned; | ||||
|       foreach (clip_txt, clip_txt_list) { | ||||
| @@ -94,7 +94,7 @@ class downloadFromURL : public QDialog, private Ui::downloadFromURL | ||||
|     void downloadButtonClicked() | ||||
|     { | ||||
|       QString urls = textUrls->toPlainText(); | ||||
|       QStringList url_list = urls.split(QString::fromUtf8("\n")); | ||||
|       QStringList url_list = urls.split(QLatin1Char('\n')); | ||||
|       QString url; | ||||
|       QStringList url_list_cleaned; | ||||
|       foreach (url, url_list) { | ||||
|   | ||||
| @@ -427,91 +427,5 @@ POSSIBILITY OF SUCH DAMAGES. | ||||
| </p>  | ||||
|   | ||||
| <h3>END OF TERMS AND CONDITIONS</h3> | ||||
|   | ||||
| <h3><a name="howto"></a><a name="SEC4" href="#TOC4">How to Apply These Terms to Your New Programs</a></h3>  | ||||
|   | ||||
| <p>  | ||||
|   If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these terms. | ||||
| </p>  | ||||
|   | ||||
| <p>  | ||||
|   To do so, attach the following notices to the program.  It is safest | ||||
| to attach them to the start of each source file to most effectively | ||||
| convey the exclusion of warranty; and each file should have at least | ||||
| the "copyright" line and a pointer to where the full notice is found. | ||||
| </p>  | ||||
|   | ||||
| <pre>  | ||||
| <var>one line to give the program's name and an idea of what it does.</var>  | ||||
| Copyright (C) <var>yyyy</var>  <var>name of author</var>  | ||||
|   | ||||
| This program is free software; you can redistribute it and/or | ||||
| modify it under the terms of the GNU General Public License | ||||
| as published by the Free Software Foundation; either version 2 | ||||
| of the License, or (at your option) any later version. | ||||
|   | ||||
| This program is distributed in the hope that it will be useful, | ||||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| GNU General Public License for more details. | ||||
|   | ||||
| You should have received a copy of the GNU General Public License | ||||
| along with this program; if not, write to the Free Software | ||||
| Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,  | ||||
| MA  02110-1301, USA. | ||||
| </pre>  | ||||
|   | ||||
| <p>  | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
| </p>  | ||||
|   | ||||
| <p>  | ||||
| If the program is interactive, make it output a short notice like this | ||||
| when it starts in an interactive mode: | ||||
| </p>  | ||||
|   | ||||
| <pre>  | ||||
| Gnomovision version 69, Copyright (C) <var>year</var> <var>name of author</var>  | ||||
| Gnomovision comes with ABSOLUTELY NO WARRANTY; for details | ||||
| type `show w'.  This is free software, and you are welcome | ||||
| to redistribute it under certain conditions; type `show c'  | ||||
| for details. | ||||
| </pre>  | ||||
|   | ||||
| <p>  | ||||
| The hypothetical commands <samp>`show w'</samp> and <samp>`show c'</samp> should show | ||||
| the appropriate parts of the General Public License.  Of course, the | ||||
| commands you use may be called something other than <samp>`show w'</samp> and | ||||
| <samp>`show c'</samp>; they could even be mouse-clicks or menu items--whatever | ||||
| suits your program. | ||||
| </p>  | ||||
|   | ||||
| <p>  | ||||
| You should also get your employer (if you work as a programmer) or your | ||||
| school, if any, to sign a "copyright disclaimer" for the program, if | ||||
| necessary.  Here is a sample; alter the names: | ||||
| </p>  | ||||
|   | ||||
|   | ||||
| <pre>  | ||||
| Yoyodyne, Inc., hereby disclaims all copyright | ||||
| interest in the program `Gnomovision' | ||||
| (which makes passes at compilers) written  | ||||
| by James Hacker. | ||||
|   | ||||
| <var>signature of Ty Coon</var>, 1 April 1989 | ||||
| Ty Coon, President of Vice | ||||
| </pre>  | ||||
|   | ||||
| <p>  | ||||
| This General Public License does not permit incorporating your program into | ||||
| proprietary programs.  If your program is a subroutine library, you may | ||||
| consider it more useful to permit linking proprietary applications with the | ||||
| library.  If this is what you want to do, use the  | ||||
| <a href="https://www.gnu.org/licenses/lgpl.html">GNU Lesser General Public License</a>  | ||||
| instead of this License. | ||||
| </p> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -55,6 +55,7 @@ HEADERS += \ | ||||
|     $$PWD/tagfilterproxymodel.h \ | ||||
|     $$PWD/tagfilterwidget.h \ | ||||
|     $$PWD/banlistoptions.h \ | ||||
|     $$PWD/ipsubnetwhitelistoptionsdialog.h \ | ||||
|     $$PWD/rss/rsswidget.h \ | ||||
|     $$PWD/rss/articlelistwidget.h \ | ||||
|     $$PWD/rss/feedlistwidget.h \ | ||||
| @@ -109,6 +110,7 @@ SOURCES += \ | ||||
|     $$PWD/tagfilterproxymodel.cpp \ | ||||
|     $$PWD/tagfilterwidget.cpp \ | ||||
|     $$PWD/banlistoptions.cpp \ | ||||
|     $$PWD/ipsubnetwhitelistoptionsdialog.cpp \ | ||||
|     $$PWD/rss/rsswidget.cpp \ | ||||
|     $$PWD/rss/articlelistwidget.cpp \ | ||||
|     $$PWD/rss/feedlistwidget.cpp \ | ||||
| @@ -150,6 +152,7 @@ FORMS += \ | ||||
|     $$PWD/search/searchtab.ui \ | ||||
|     $$PWD/cookiesdialog.ui \ | ||||
|     $$PWD/banlistoptions.ui \ | ||||
|     $$PWD/ipsubnetwhitelistoptionsdialog.ui \ | ||||
|     $$PWD/rss/rsswidget.ui \ | ||||
|     $$PWD/rss/automatedrssdownloader.ui \ | ||||
|     $$PWD/torrentcategorydialog.ui | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user