1 /*
2  *  This file is part of nzbget. See <http://nzbget.net>.
3  *
4  *  Copyright (C) 2007-2017 Andrey Prygunkov <hugbug@users.sourceforge.net>
5  *
6  *  This program is free software; you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation; either version 2 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 
21 #include "nzbget.h"
22 #include "NString.h"
23 #include "QueueScript.h"
24 #include "NzbScript.h"
25 #include "Options.h"
26 #include "Log.h"
27 #include "Util.h"
28 #include "FileSystem.h"
29 
30 static const char* QUEUE_EVENT_NAMES[] = {
31 	"FILE_DOWNLOADED",
32 	"URL_COMPLETED",
33 	"NZB_MARKED",
34 	"NZB_ADDED",
35 	"NZB_NAMED",
36 	"NZB_DOWNLOADED",
37 	"NZB_DELETED" };
38 
39 class QueueScriptController : public Thread, public NzbScriptController
40 {
41 public:
42 	virtual void Run();
43 	static void StartScript(NzbInfo* nzbInfo, ScriptConfig::Script* script, QueueScriptCoordinator::EEvent event);
44 
45 protected:
46 	virtual void ExecuteScript(ScriptConfig::Script* script);
47 	virtual void AddMessage(Message::EKind kind, const char* text);
48 
49 private:
50 	CString m_nzbName;
51 	CString m_nzbFilename;
52 	CString m_url;
53 	CString m_category;
54 	CString m_destDir;
55 	CString m_queuedFilename;
56 	int m_id;
57 	int m_priority;
58 	CString m_dupeKey;
59 	EDupeMode m_dupeMode;
60 	int m_dupeScore;
61 	NzbParameterList m_parameters;
62 	int m_prefixLen;
63 	ScriptConfig::Script* m_script;
64 	QueueScriptCoordinator::EEvent m_event;
65 	bool m_markBad;
66 	NzbInfo::EDeleteStatus m_deleteStatus;
67 	NzbInfo::EUrlStatus m_urlStatus;
68 	NzbInfo::EMarkStatus m_markStatus;
69 
70 	void PrepareParams(const char* scriptName);
71 };
72 
73 
StartScript(NzbInfo * nzbInfo,ScriptConfig::Script * script,QueueScriptCoordinator::EEvent event)74 void QueueScriptController::StartScript(NzbInfo* nzbInfo, ScriptConfig::Script* script, QueueScriptCoordinator::EEvent event)
75 {
76 	QueueScriptController* scriptController = new QueueScriptController();
77 
78 	scriptController->m_nzbName = nzbInfo->GetName();
79 	scriptController->m_nzbFilename = nzbInfo->GetFilename();
80 	scriptController->m_url = nzbInfo->GetUrl();
81 	scriptController->m_category = nzbInfo->GetCategory();
82 	scriptController->m_destDir = nzbInfo->GetDestDir();
83 	scriptController->m_queuedFilename = nzbInfo->GetQueuedFilename();
84 	scriptController->m_id = nzbInfo->GetId();
85 	scriptController->m_priority = nzbInfo->GetPriority();
86 	scriptController->m_dupeKey = nzbInfo->GetDupeKey();
87 	scriptController->m_dupeMode = nzbInfo->GetDupeMode();
88 	scriptController->m_dupeScore = nzbInfo->GetDupeScore();
89 	scriptController->m_parameters.CopyFrom(nzbInfo->GetParameters());
90 	scriptController->m_script = script;
91 	scriptController->m_event = event;
92 	scriptController->m_prefixLen = 0;
93 	scriptController->m_markBad = false;
94 	scriptController->m_deleteStatus = nzbInfo->GetDeleteStatus();
95 	scriptController->m_urlStatus = nzbInfo->GetUrlStatus();
96 	scriptController->m_markStatus = nzbInfo->GetMarkStatus();
97 	scriptController->SetAutoDestroy(true);
98 
99 	scriptController->Start();
100 }
101 
Run()102 void QueueScriptController::Run()
103 {
104 	ExecuteScript(m_script);
105 
106 	SetLogPrefix(nullptr);
107 
108 	if (m_markBad)
109 	{
110 		GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
111 		NzbInfo* nzbInfo = downloadQueue->GetQueue()->Find(m_id);
112 		if (nzbInfo)
113 		{
114 			nzbInfo->PrintMessage(Message::mkWarning, "Cancelling download and deleting %s", *m_nzbName);
115 			nzbInfo->SetDeleteStatus(NzbInfo::dsBad);
116 			downloadQueue->EditEntry(m_id, DownloadQueue::eaGroupDelete, nullptr);
117 		}
118 	}
119 
120 	g_QueueScriptCoordinator->CheckQueue();
121 }
122 
ExecuteScript(ScriptConfig::Script * script)123 void QueueScriptController::ExecuteScript(ScriptConfig::Script* script)
124 {
125 	PrintMessage(m_event == QueueScriptCoordinator::qeFileDownloaded ? Message::mkDetail : Message::mkInfo,
126 		"Executing queue-script %s for %s", script->GetName(), FileSystem::BaseFileName(m_nzbName));
127 
128 	SetArgs({script->GetLocation()});
129 
130 	BString<1024> infoName("queue-script %s for %s", script->GetName(), FileSystem::BaseFileName(m_nzbName));
131 	SetInfoName(infoName);
132 
133 	SetLogPrefix(script->GetDisplayName());
134 	m_prefixLen = strlen(script->GetDisplayName()) + 2; // 2 = strlen(": ");
135 	PrepareParams(script->GetName());
136 
137 	Execute();
138 
139 	SetLogPrefix(nullptr);
140 }
141 
PrepareParams(const char * scriptName)142 void QueueScriptController::PrepareParams(const char* scriptName)
143 {
144 	ResetEnv();
145 
146 	SetEnvVar("NZBNA_NZBNAME", m_nzbName);
147 	SetIntEnvVar("NZBNA_NZBID", m_id);
148 	SetEnvVar("NZBNA_FILENAME", m_nzbFilename);
149 	SetEnvVar("NZBNA_DIRECTORY", m_destDir);
150 	SetEnvVar("NZBNA_QUEUEDFILE", m_queuedFilename);
151 	SetEnvVar("NZBNA_URL", m_url);
152 	SetEnvVar("NZBNA_CATEGORY", m_category);
153 	SetIntEnvVar("NZBNA_PRIORITY", m_priority);
154 	SetIntEnvVar("NZBNA_LASTID", m_id);	// deprecated
155 
156 	SetEnvVar("NZBNA_DUPEKEY", m_dupeKey);
157 	SetIntEnvVar("NZBNA_DUPESCORE", m_dupeScore);
158 
159 	const char* dupeModeName[] = { "SCORE", "ALL", "FORCE" };
160 	SetEnvVar("NZBNA_DUPEMODE", dupeModeName[m_dupeMode]);
161 
162 	SetEnvVar("NZBNA_EVENT", QUEUE_EVENT_NAMES[m_event]);
163 
164 	const char* deleteStatusName[] = { "NONE", "MANUAL", "HEALTH", "DUPE", "BAD", "GOOD", "COPY", "SCAN" };
165 	SetEnvVar("NZBNA_DELETESTATUS", deleteStatusName[m_deleteStatus]);
166 
167 	const char* urlStatusName[] = { "NONE", "UNKNOWN", "SUCCESS", "FAILURE", "UNKNOWN", "SCAN_SKIPPED", "SCAN_FAILURE" };
168 	SetEnvVar("NZBNA_URLSTATUS", urlStatusName[m_urlStatus]);
169 
170 	const char* markStatusName[] = { "NONE", "BAD", "GOOD", "SUCCESS" };
171 	SetEnvVar("NZBNA_MARKSTATUS", markStatusName[m_markStatus]);
172 
173 	PrepareEnvScript(&m_parameters, scriptName);
174 }
175 
AddMessage(Message::EKind kind,const char * text)176 void QueueScriptController::AddMessage(Message::EKind kind, const char* text)
177 {
178 	const char* msgText = text + m_prefixLen;
179 
180 	if (!strncmp(msgText, "[NZB] ", 6))
181 	{
182 		debug("Command %s detected", msgText + 6);
183 		if (!strncmp(msgText + 6, "NZBPR_", 6))
184 		{
185 			CString param = msgText + 6 + 6;
186 			char* value = strchr(param, '=');
187 			if (value)
188 			{
189 				*value = '\0';
190 				GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
191 				NzbInfo* nzbInfo = QueueScriptCoordinator::FindNzbInfo(downloadQueue, m_id);
192 				if (nzbInfo)
193 				{
194 					nzbInfo->GetParameters()->SetParameter(param, value + 1);
195 				}
196 			}
197 			else
198 			{
199 				error("Invalid command \"%s\" received from %s", msgText, GetInfoName());
200 			}
201 		}
202 		else if (!strncmp(msgText + 6, "DIRECTORY=", 10) &&
203 			m_event == QueueScriptCoordinator::qeNzbDownloaded)
204 		{
205 			GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
206 			NzbInfo* nzbInfo = QueueScriptCoordinator::FindNzbInfo(downloadQueue, m_id);
207 			if (nzbInfo)
208 			{
209 				nzbInfo->SetFinalDir(msgText + 6 + 10);
210 			}
211 		}
212 		else if (!strncmp(msgText + 6, "MARK=BAD", 8))
213 		{
214 			m_markBad = true;
215 			GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
216 			NzbInfo* nzbInfo = QueueScriptCoordinator::FindNzbInfo(downloadQueue, m_id);
217 			if (nzbInfo)
218 			{
219 				nzbInfo->PrintMessage(Message::mkWarning, "Marking %s as bad", *m_nzbName);
220 				nzbInfo->SetMarkStatus(NzbInfo::ksBad);
221 			}
222 		}
223 		else
224 		{
225 			error("Invalid command \"%s\" received from %s", msgText, GetInfoName());
226 		}
227 	}
228 	else
229 	{
230 		NzbInfo* nzbInfo = nullptr;
231 		{
232 			GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
233 			nzbInfo = QueueScriptCoordinator::FindNzbInfo(downloadQueue, m_id);
234 			if (nzbInfo)
235 			{
236 				nzbInfo->AddMessage(kind, text);
237 			}
238 		}
239 
240 		if (!nzbInfo)
241 		{
242 			ScriptController::AddMessage(kind, text);
243 		}
244 	}
245 }
246 
247 
InitOptions()248 void QueueScriptCoordinator::InitOptions()
249 {
250 	m_hasQueueScripts = false;
251 	for (ScriptConfig::Script& script : g_ScriptConfig->GetScripts())
252 	{
253 		if (script.GetQueueScript())
254 		{
255 			m_hasQueueScripts = true;
256 			break;
257 		}
258 	}
259 }
260 
EnqueueScript(NzbInfo * nzbInfo,EEvent event)261 void QueueScriptCoordinator::EnqueueScript(NzbInfo* nzbInfo, EEvent event)
262 {
263 	if (!m_hasQueueScripts)
264 	{
265 		return;
266 	}
267 
268 	Guard guard(m_queueMutex);
269 
270 	if (event == qeNzbDownloaded)
271 	{
272 		// delete all other queued scripts for this nzb
273 		m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(),
274 			[nzbInfo](std::unique_ptr<QueueItem>& queueItem)
275 			{
276 				return queueItem->GetNzbId() == nzbInfo->GetId();
277 			}),
278 			m_queue.end());
279 	}
280 
281 	// respect option "EventInterval"
282 	time_t curTime = Util::CurrentTime();
283 	if (event == qeFileDownloaded &&
284 		(g_Options->GetEventInterval() == -1 ||
285 		 (g_Options->GetEventInterval() > 0 && curTime - nzbInfo->GetQueueScriptTime() > 0 &&
286 		 (int)(curTime - nzbInfo->GetQueueScriptTime()) < g_Options->GetEventInterval())))
287 	{
288 		return;
289 	}
290 
291 	for (ScriptConfig::Script& script : g_ScriptConfig->GetScripts())
292 	{
293 		if (UsableScript(script, nzbInfo, event))
294 		{
295 			bool alreadyQueued = false;
296 			if (event == qeFileDownloaded)
297 			{
298 				// check if this script is already queued for this nzb
299 				for (QueueItem* queueItem : &m_queue)
300 				{
301 					if (queueItem->GetNzbId() == nzbInfo->GetId() && queueItem->GetScript() == &script)
302 					{
303 						alreadyQueued = true;
304 						break;
305 					}
306 				}
307 			}
308 
309 			if (!alreadyQueued)
310 			{
311 				std::unique_ptr<QueueItem> queueItem = std::make_unique<QueueItem>(nzbInfo->GetId(), &script, event);
312 				if (m_curItem)
313 				{
314 					m_queue.push_back(std::move(queueItem));
315 				}
316 				else
317 				{
318 					m_curItem = std::move(queueItem);
319 					QueueScriptController::StartScript(nzbInfo, m_curItem->GetScript(), m_curItem->GetEvent());
320 				}
321 			}
322 
323 			nzbInfo->SetQueueScriptTime(Util::CurrentTime());
324 		}
325 	}
326 }
327 
UsableScript(ScriptConfig::Script & script,NzbInfo * nzbInfo,EEvent event)328 bool QueueScriptCoordinator::UsableScript(ScriptConfig::Script& script, NzbInfo* nzbInfo, EEvent event)
329 {
330 	if (!script.GetQueueScript())
331 	{
332 		return false;
333 	}
334 
335 	if (!Util::EmptyStr(script.GetQueueEvents()) && !strstr(script.GetQueueEvents(), QUEUE_EVENT_NAMES[event]))
336 	{
337 		return false;
338 	}
339 
340 	// check extension scripts assigned for that nzb
341 	for (NzbParameter& parameter : nzbInfo->GetParameters())
342 	{
343 		const char* varname = parameter.GetName();
344 		if (strlen(varname) > 0 && varname[0] != '*' && varname[strlen(varname)-1] == ':' &&
345 			(!strcasecmp(parameter.GetValue(), "yes") ||
346 			 !strcasecmp(parameter.GetValue(), "on") ||
347 			 !strcasecmp(parameter.GetValue(), "1")))
348 		{
349 			BString<1024> scriptName = varname;
350 			scriptName[strlen(scriptName)-1] = '\0'; // remove trailing ':'
351 			if (FileSystem::SameFilename(scriptName, script.GetName()))
352 			{
353 				return true;
354 			}
355 		}
356 	}
357 
358 	// for URL-events the extension scripts are not assigned yet;
359 	// instead we take the default extension scripts for the category (or global)
360 	if (event == qeUrlCompleted)
361 	{
362 		const char* postScript = g_Options->GetExtensions();
363 		if (!Util::EmptyStr(nzbInfo->GetCategory()))
364 		{
365 			Options::Category* categoryObj = g_Options->FindCategory(nzbInfo->GetCategory(), false);
366 			if (categoryObj && !Util::EmptyStr(categoryObj->GetExtensions()))
367 			{
368 				postScript = categoryObj->GetExtensions();
369 			}
370 		}
371 
372 		if (!Util::EmptyStr(postScript))
373 		{
374 			Tokenizer tok(postScript, ",;");
375 			while (const char* scriptName = tok.Next())
376 			{
377 				if (FileSystem::SameFilename(scriptName, script.GetName()))
378 				{
379 					return true;
380 				}
381 			}
382 		}
383 	}
384 
385 	return false;
386 }
387 
FindNzbInfo(DownloadQueue * downloadQueue,int nzbId)388 NzbInfo* QueueScriptCoordinator::FindNzbInfo(DownloadQueue* downloadQueue, int nzbId)
389 {
390 	NzbInfo* nzbInfo = downloadQueue->GetQueue()->Find(nzbId);
391 	if (nzbInfo)
392 	{
393 		return nzbInfo;
394 	}
395 
396 	HistoryInfo* historyInfo = downloadQueue->GetHistory()->Find(nzbId);
397 	if (historyInfo)
398 	{
399 		return historyInfo->GetNzbInfo();
400 	}
401 
402 	return nullptr;
403 }
404 
CheckQueue()405 void QueueScriptCoordinator::CheckQueue()
406 {
407 	if (m_stopped)
408 	{
409 		return;
410 	}
411 
412 	GuardedDownloadQueue downloadQueue = DownloadQueue::Guard();
413 	Guard guard(m_queueMutex);
414 
415 	m_curItem.reset();
416 	NzbInfo* curNzbInfo = nullptr;
417 	Queue::iterator itCurItem;
418 
419 	for (Queue::iterator it = m_queue.begin(); it != m_queue.end(); )
420 	{
421 		std::unique_ptr<QueueItem>& queueItem = *it;
422 
423 		NzbInfo* nzbInfo = FindNzbInfo(downloadQueue, queueItem->GetNzbId());
424 
425 		// in a case this nzb must not be processed further - delete queue script from queue
426 		EEvent event = queueItem->GetEvent();
427 		bool ignoreEvent = !nzbInfo ||
428 			(nzbInfo->GetDeleteStatus() != NzbInfo::dsNone && event != qeNzbDeleted && event != qeNzbMarked) ||
429 			(nzbInfo->GetMarkStatus() == NzbInfo::ksBad && event != qeNzbMarked);
430 
431 		if (ignoreEvent)
432 		{
433 			it = m_queue.erase(it);
434 			if (curNzbInfo)
435 			{
436 				// process from the beginning, while "erase" invalidated "itCurItem"
437 				curNzbInfo = nullptr;
438 				it = m_queue.begin();
439 			}
440 			continue;
441 		}
442 
443 		if (!m_curItem || queueItem->GetEvent() > m_curItem->GetEvent())
444 		{
445 			itCurItem = it;
446 			curNzbInfo = nzbInfo;
447 		}
448 
449 		it++;
450 	}
451 
452 	if (curNzbInfo)
453 	{
454 		m_curItem = std::move(*itCurItem);
455 		m_queue.erase(itCurItem);
456 		QueueScriptController::StartScript(curNzbInfo, m_curItem->GetScript(), m_curItem->GetEvent());
457 	}
458 }
459 
HasJob(int nzbId,bool * active)460 bool QueueScriptCoordinator::HasJob(int nzbId, bool* active)
461 {
462 	Guard guard(m_queueMutex);
463 
464 	bool working = m_curItem && m_curItem->GetNzbId() == nzbId;
465 	if (active)
466 	{
467 		*active = working;
468 	}
469 	if (!working)
470 	{
471 		for (QueueItem* queueItem : &m_queue)
472 		{
473 			working = queueItem->GetNzbId() == nzbId;
474 			if (working)
475 			{
476 				break;
477 			}
478 		}
479 	}
480 
481 	return working;
482 }
483 
GetQueueSize()484 int QueueScriptCoordinator::GetQueueSize()
485 {
486 	Guard guard(m_queueMutex);
487 
488 	int queuedCount = m_queue.size();
489 	if (m_curItem)
490 	{
491 		queuedCount++;
492 	}
493 
494 	return queuedCount;
495 }
496