1 /*
2  *  This file is part of nzbget. See <http://nzbget.net>.
3  *
4  *  Copyright (C) 2013-2016 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 "Util.h"
23 #include "FileSystem.h"
24 #include "Options.h"
25 #include "Log.h"
26 #include "ScriptConfig.h"
27 
28 static const char* BEGIN_SCRIPT_SIGNATURE = "### NZBGET ";
29 static const char* POST_SCRIPT_SIGNATURE = "POST-PROCESSING";
30 static const char* SCAN_SCRIPT_SIGNATURE = "SCAN";
31 static const char* QUEUE_SCRIPT_SIGNATURE = "QUEUE";
32 static const char* SCHEDULER_SCRIPT_SIGNATURE = "SCHEDULER";
33 static const char* FEED_SCRIPT_SIGNATURE = "FEED";
34 static const char* END_SCRIPT_SIGNATURE = " SCRIPT";
35 static const char* QUEUE_EVENTS_SIGNATURE = "### QUEUE EVENTS:";
36 static const char* TASK_TIME_SIGNATURE = "### TASK TIME:";
37 static const char* DEFINITION_SIGNATURE = "###";
38 
InitOptions()39 void ScriptConfig::InitOptions()
40 {
41 	InitScripts();
42 	InitConfigTemplates();
43 	CreateTasks();
44 }
45 
LoadConfig(Options::OptEntries * optEntries)46 bool ScriptConfig::LoadConfig(Options::OptEntries* optEntries)
47 {
48 	// read config file
49 	DiskFile infile;
50 
51 	if (!infile.Open(g_Options->GetConfigFilename(), DiskFile::omRead))
52 	{
53 		return false;
54 	}
55 
56 	int fileLen = (int)FileSystem::FileSize(g_Options->GetConfigFilename());
57 	CString buf;
58 	buf.Reserve(fileLen);
59 
60 	while (infile.ReadLine(buf, fileLen + 1))
61 	{
62 		// remove trailing '\n' and '\r' and spaces
63 		Util::TrimRight(buf);
64 
65 		// skip comments and empty lines
66 		if (buf[0] == 0 || buf[0] == '#' || strspn(buf, " ") == strlen(buf))
67 		{
68 			continue;
69 		}
70 
71 		CString optname;
72 		CString optvalue;
73 		if (Options::SplitOptionString(buf, optname, optvalue))
74 		{
75 			optEntries->emplace_back(optname, optvalue);
76 		}
77 	}
78 
79 	infile.Close();
80 
81 	Options::ConvertOldOptions(optEntries);
82 
83 	return true;
84 }
85 
SaveConfig(Options::OptEntries * optEntries)86 bool ScriptConfig::SaveConfig(Options::OptEntries* optEntries)
87 {
88 	// save to config file
89 	DiskFile infile;
90 
91 	if (!infile.Open(g_Options->GetConfigFilename(), DiskFile::omReadWrite))
92 	{
93 		return false;
94 	}
95 
96 	std::vector<CString> config;
97 	std::set<Options::OptEntry*> writtenOptions;
98 
99 	// read config file into memory array
100 	int fileLen = (int)FileSystem::FileSize(g_Options->GetConfigFilename()) + 1;
101 	CString content;
102 	content.Reserve(fileLen);
103 	while (infile.ReadLine(content, fileLen + 1))
104 	{
105 		config.push_back(*content);
106 	}
107 	content.Clear();
108 
109 	// write config file back to disk, replace old values of existing options with new values
110 	infile.Seek(0);
111 	for (CString& buf : config)
112 	{
113 		const char* eq = strchr(buf, '=');
114 		if (eq && buf[0] != '#')
115 		{
116 			// remove trailing '\n' and '\r' and spaces
117 			buf.TrimRight();
118 
119 			CString optname;
120 			CString optvalue;
121 			if (g_Options->SplitOptionString(buf, optname, optvalue))
122 			{
123 				Options::OptEntry* optEntry = optEntries->FindOption(optname);
124 				if (optEntry)
125 				{
126 					infile.Print("%s=%s\n", optEntry->GetName(), optEntry->GetValue());
127 					writtenOptions.insert(optEntry);
128 				}
129 			}
130 		}
131 		else
132 		{
133 			infile.Print("%s", *buf);
134 		}
135 	}
136 
137 	// write new options
138 	for (Options::OptEntry& optEntry : *optEntries)
139 	{
140 		std::set<Options::OptEntry*>::iterator fit = writtenOptions.find(&optEntry);
141 		if (fit == writtenOptions.end())
142 		{
143 			infile.Print("%s=%s\n", optEntry.GetName(), optEntry.GetValue());
144 		}
145 	}
146 
147 	// close and truncate the file
148 	int pos = (int)infile.Position();
149 	infile.Close();
150 
151 	FileSystem::TruncateFile(g_Options->GetConfigFilename(), pos);
152 
153 	return true;
154 }
155 
LoadConfigTemplates(ConfigTemplates * configTemplates)156 bool ScriptConfig::LoadConfigTemplates(ConfigTemplates* configTemplates)
157 {
158 	CharBuffer buffer;
159 	if (!FileSystem::LoadFileIntoBuffer(g_Options->GetConfigTemplate(), buffer, true))
160 	{
161 		return false;
162 	}
163 	configTemplates->emplace_back(Script("", ""), buffer);
164 
165 	if (!g_Options->GetScriptDir())
166 	{
167 		return true;
168 	}
169 
170 	Scripts scriptList;
171 	LoadScripts(&scriptList);
172 
173 	const int beginSignatureLen = strlen(BEGIN_SCRIPT_SIGNATURE);
174 	const int definitionSignatureLen = strlen(DEFINITION_SIGNATURE);
175 
176 	for (Script& script : scriptList)
177 	{
178 		DiskFile infile;
179 		if (!infile.Open(script.GetLocation(), DiskFile::omRead))
180 		{
181 			configTemplates->emplace_back(std::move(script), "");
182 			continue;
183 		}
184 
185 		StringBuilder templ;
186 		char buf[1024];
187 		bool inConfig = false;
188 		bool inHeader = false;
189 
190 		while (infile.ReadLine(buf, sizeof(buf) - 1))
191 		{
192 			if (!strncmp(buf, BEGIN_SCRIPT_SIGNATURE, beginSignatureLen) &&
193 				strstr(buf, END_SCRIPT_SIGNATURE) &&
194 				(strstr(buf, POST_SCRIPT_SIGNATURE) ||
195 				 strstr(buf, SCAN_SCRIPT_SIGNATURE) ||
196 				 strstr(buf, QUEUE_SCRIPT_SIGNATURE) ||
197 				 strstr(buf, SCHEDULER_SCRIPT_SIGNATURE) ||
198 				 strstr(buf, FEED_SCRIPT_SIGNATURE)))
199 			{
200 				if (inConfig)
201 				{
202 					break;
203 				}
204 				inConfig = true;
205 				inHeader = true;
206 				continue;
207 			}
208 
209 			inHeader &= !strncmp(buf, DEFINITION_SIGNATURE, definitionSignatureLen);
210 
211 			if (inConfig && !inHeader)
212 			{
213 				templ.Append(buf);
214 			}
215 		}
216 
217 		infile.Close();
218 
219 		configTemplates->emplace_back(std::move(script), templ);
220 	}
221 
222 	return true;
223 }
224 
InitConfigTemplates()225 void ScriptConfig::InitConfigTemplates()
226 {
227 	if (!LoadConfigTemplates(&m_configTemplates))
228 	{
229 		error("Could not read configuration templates");
230 	}
231 }
232 
InitScripts()233 void ScriptConfig::InitScripts()
234 {
235 	LoadScripts(&m_scripts);
236 }
237 
LoadScripts(Scripts * scripts)238 void ScriptConfig::LoadScripts(Scripts* scripts)
239 {
240 	if (Util::EmptyStr(g_Options->GetScriptDir()))
241 	{
242 		return;
243 	}
244 
245 	Scripts tmpScripts;
246 
247 	Tokenizer tokDir(g_Options->GetScriptDir(), ",;");
248 	while (const char* scriptDir = tokDir.Next())
249 	{
250 		LoadScriptDir(&tmpScripts, scriptDir, false);
251 	}
252 
253 	tmpScripts.sort(
254 		[](Script& script1, Script& script2)
255 		{
256 			return strcmp(script1.GetName(), script2.GetName()) < 0;
257 		});
258 
259 	// first add all scripts from ScriptOrder
260 	Tokenizer tokOrder(g_Options->GetScriptOrder(), ",;");
261 	while (const char* scriptName = tokOrder.Next())
262 	{
263 		Scripts::iterator pos = std::find_if(tmpScripts.begin(), tmpScripts.end(),
264 			[scriptName](Script& script)
265 			{
266 				return !strcmp(script.GetName(), scriptName);
267 			});
268 
269 		if (pos != tmpScripts.end())
270 		{
271 			scripts->splice(scripts->end(), tmpScripts, pos);
272 		}
273 	}
274 
275 	// then add all other scripts from scripts directory
276 	scripts->splice(scripts->end(), std::move(tmpScripts));
277 
278 	BuildScriptDisplayNames(scripts);
279 }
280 
LoadScriptDir(Scripts * scripts,const char * directory,bool isSubDir)281 void ScriptConfig::LoadScriptDir(Scripts* scripts, const char* directory, bool isSubDir)
282 {
283 	DirBrowser dir(directory);
284 	while (const char* filename = dir.Next())
285 	{
286 		if (filename[0] != '.' && filename[0] != '_')
287 		{
288 			BString<1024> fullFilename("%s%c%s", directory, PATH_SEPARATOR, filename);
289 
290 			if (!FileSystem::DirectoryExists(fullFilename))
291 			{
292 				BString<1024> scriptName = BuildScriptName(directory, filename, isSubDir);
293 				if (ScriptExists(scripts, scriptName))
294 				{
295 					continue;
296 				}
297 
298 				Script script(scriptName, fullFilename);
299 				if (LoadScriptFile(&script))
300 				{
301 					scripts->push_back(std::move(script));
302 				}
303 			}
304 			else if (!isSubDir)
305 			{
306 				LoadScriptDir(scripts, fullFilename, true);
307 			}
308 		}
309 	}
310 }
311 
LoadScriptFile(Script * script)312 bool ScriptConfig::LoadScriptFile(Script* script)
313 {
314 	DiskFile infile;
315 	if (!infile.Open(script->GetLocation(), DiskFile::omRead))
316 	{
317 		return false;
318 	}
319 
320 	CharBuffer buffer(1024 * 10 + 1);
321 
322 	const int beginSignatureLen = strlen(BEGIN_SCRIPT_SIGNATURE);
323 	const int queueEventsSignatureLen = strlen(QUEUE_EVENTS_SIGNATURE);
324 	const int taskTimeSignatureLen = strlen(TASK_TIME_SIGNATURE);
325 	const int definitionSignatureLen = strlen(DEFINITION_SIGNATURE);
326 
327 	// check if the file contains pp-script-signature
328 	// read first 10KB of the file and look for signature
329 	int readBytes = (int)infile.Read(buffer, buffer.Size() - 1);
330 	infile.Close();
331 	buffer[readBytes] = '\0';
332 
333 	bool postScript = false;
334 	bool scanScript = false;
335 	bool queueScript = false;
336 	bool schedulerScript = false;
337 	bool feedScript = false;
338 	char* queueEvents = nullptr;
339 	char* taskTime = nullptr;
340 
341 	bool inConfig = false;
342 	bool afterConfig = false;
343 
344 	// Declarations "QUEUE EVENT:" and "TASK TIME:" can be placed:
345 	// - in script definition body (between opening and closing script signatures);
346 	// - immediately before script definition (before opening script signature);
347 	// - immediately after script definition (after closing script signature).
348 	// The last two pissibilities are provided to increase compatibility of scripts with older
349 	// nzbget versions which do not expect the extra declarations in the script defintion body.
350 
351 	Tokenizer tok(buffer, "\n\r", true);
352 	while (char* line = tok.Next())
353 	{
354 		if (!strncmp(line, QUEUE_EVENTS_SIGNATURE, queueEventsSignatureLen))
355 		{
356 			queueEvents = line + queueEventsSignatureLen;
357 		}
358 		else if (!strncmp(line, TASK_TIME_SIGNATURE, taskTimeSignatureLen))
359 		{
360 			taskTime = line + taskTimeSignatureLen;
361 		}
362 
363 		bool header = !strncmp(line, DEFINITION_SIGNATURE, definitionSignatureLen);
364 		if (!header && !inConfig)
365 		{
366 			queueEvents = nullptr;
367 			taskTime = nullptr;
368 		}
369 
370 		if (!header && afterConfig)
371 		{
372 			break;
373 		}
374 
375 		if (!strncmp(line, BEGIN_SCRIPT_SIGNATURE, beginSignatureLen) && strstr(line, END_SCRIPT_SIGNATURE))
376 		{
377 			if (!inConfig)
378 			{
379 				inConfig = true;
380 				postScript = strstr(line, POST_SCRIPT_SIGNATURE);
381 				scanScript = strstr(line, SCAN_SCRIPT_SIGNATURE);
382 				queueScript = strstr(line, QUEUE_SCRIPT_SIGNATURE);
383 				schedulerScript = strstr(line, SCHEDULER_SCRIPT_SIGNATURE);
384 				feedScript = strstr(line, FEED_SCRIPT_SIGNATURE);
385 			}
386 			else
387 			{
388 				afterConfig = true;
389 			}
390 		}
391 	}
392 
393 	if (!(postScript || scanScript || queueScript || schedulerScript || feedScript))
394 	{
395 		return false;
396 	}
397 
398 	// trim decorations
399 	char* p;
400 	while (queueEvents && *queueEvents && *(p = queueEvents + strlen(queueEvents) - 1) == '#') *p = '\0';
401 	if (queueEvents) queueEvents = Util::Trim(queueEvents);
402 	while (taskTime && *taskTime && *(p = taskTime + strlen(taskTime) - 1) == '#') *p = '\0';
403 	if (taskTime) taskTime = Util::Trim(taskTime);
404 
405 	script->SetPostScript(postScript);
406 	script->SetScanScript(scanScript);
407 	script->SetQueueScript(queueScript);
408 	script->SetSchedulerScript(schedulerScript);
409 	script->SetFeedScript(feedScript);
410 	script->SetQueueEvents(queueEvents);
411 	script->SetTaskTime(taskTime);
412 
413 	return true;
414 }
415 
BuildScriptName(const char * directory,const char * filename,bool isSubDir)416 BString<1024> ScriptConfig::BuildScriptName(const char* directory, const char* filename, bool isSubDir)
417 {
418 	if (isSubDir)
419 	{
420 		BString<1024> directory2 = directory;
421 		int len = strlen(directory2);
422 		if (directory2[len-1] == PATH_SEPARATOR || directory2[len-1] == ALT_PATH_SEPARATOR)
423 		{
424 			// trim last path-separator
425 			directory2[len-1] = '\0';
426 		}
427 
428 		return BString<1024>("%s%c%s", FileSystem::BaseFileName(directory2), PATH_SEPARATOR, filename);
429 	}
430 	else
431 	{
432 		return filename;
433 	}
434 }
435 
ScriptExists(Scripts * scripts,const char * scriptName)436 bool ScriptConfig::ScriptExists(Scripts* scripts, const char* scriptName)
437 {
438 	return std::find_if(scripts->begin(), scripts->end(),
439 		[scriptName](Script& script)
440 		{
441 			return !strcmp(script.GetName(), scriptName);
442 		}) != scripts->end();
443 }
444 
BuildScriptDisplayNames(Scripts * scripts)445 void ScriptConfig::BuildScriptDisplayNames(Scripts* scripts)
446 {
447 	// trying to use short name without path and extension.
448 	// if there are other scripts with the same short name - using a longer name instead (with ot without extension)
449 
450 	for (Script& script : scripts)
451 	{
452 		BString<1024> shortName = script.GetName();
453 		if (char* ext = strrchr(shortName, '.')) *ext = '\0'; // strip file extension
454 
455 		const char* displayName = FileSystem::BaseFileName(shortName);
456 
457 		for (Script& script2 : scripts)
458 		{
459 			BString<1024> shortName2 = script2.GetName();
460 			if (char* ext = strrchr(shortName2, '.')) *ext = '\0'; // strip file extension
461 
462 			const char* displayName2 = FileSystem::BaseFileName(shortName2);
463 
464 			if (!strcmp(displayName, displayName2) && script.GetName() != script2.GetName())
465 			{
466 				if (!strcmp(shortName, shortName2))
467 				{
468 					displayName = script.GetName();
469 				}
470 				else
471 				{
472 					displayName = shortName;
473 				}
474 				break;
475 			}
476 		}
477 
478 		script.SetDisplayName(displayName);
479 	}
480 }
481 
CreateTasks()482 void ScriptConfig::CreateTasks()
483 {
484 	for (Script& script : m_scripts)
485 	{
486 		if (script.GetSchedulerScript() && !Util::EmptyStr(script.GetTaskTime()))
487 		{
488 			Tokenizer tok(g_Options->GetExtensions(), ",;");
489 			while (const char* scriptName = tok.Next())
490 			{
491 				if (FileSystem::SameFilename(scriptName, script.GetName()))
492 				{
493 					g_Options->CreateSchedulerTask(0, script.GetTaskTime(),
494 						nullptr, Options::scScript, script.GetName());
495 					break;
496 				}
497 			}
498 		}
499 	}
500 }
501