1 /*
2   Copyright 2021 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <files_edit.h>
26 
27 #include <actuator.h>
28 #include <eval_context.h>
29 #include <files_names.h>
30 #include <files_interfaces.h>
31 #include <files_operators.h>
32 #include <files_lib.h>
33 #include <files_editxml.h>
34 #include <item_lib.h>
35 #include <policy.h>
36 
37 /*****************************************************************************/
38 
NewEditContext(char * filename,const Attributes * a)39 EditContext *NewEditContext(char *filename, const Attributes *a)
40 {
41     EditContext *ec;
42 
43     if (!IsAbsoluteFileName(filename))
44     {
45         Log(LOG_LEVEL_ERR, "Relative file name '%s' was marked for editing but has no invariant meaning", filename);
46         return NULL;
47     }
48 
49     ec = xcalloc(1, sizeof(EditContext));
50 
51     ec->filename = filename;
52 
53     /* If making changes in chroot, we need to load the file from the chroot
54      * instead. */
55     if (ChrootChanges())
56     {
57         ec->changes_filename = xstrdup(ToChangesChroot(filename));
58     }
59     else
60     {
61         ec->changes_filename = xstrdup(filename);
62     }
63 
64     ec->new_line_mode = FileNewLineMode(ec->changes_filename);
65 
66     if (a->haveeditline)
67     {
68         if (!LoadFileAsItemList(&(ec->file_start), ec->changes_filename, a->edits))
69         {
70             free(ec);
71             return NULL;
72         }
73     }
74 
75     if (a->haveeditxml)
76     {
77 #ifdef HAVE_LIBXML2
78         if (!LoadFileAsXmlDoc(&(ec->xmldoc), ec->changes_filename, a->edits))
79         {
80             free(ec);
81             return NULL;
82         }
83 #else
84         Log(LOG_LEVEL_ERR, "Cannot edit XML files without LIBXML2");
85         free(ec);
86         return NULL;
87 #endif
88     }
89 
90     if (a->edits.empty_before_use)
91     {
92         Log(LOG_LEVEL_VERBOSE, "Build file model from a blank slate (emptying)");
93         DeleteItemList(ec->file_start);
94         ec->file_start = NULL;
95     }
96 
97     return ec;
98 }
99 
100 /*****************************************************************************/
101 
FinishEditContext(EvalContext * ctx,EditContext * ec,const Attributes * a,const Promise * pp,PromiseResult * result)102 void FinishEditContext(EvalContext *ctx, EditContext *ec, const Attributes *a, const Promise *pp,
103                        PromiseResult *result)
104 {
105     if ((*result != PROMISE_RESULT_NOOP) && (*result != PROMISE_RESULT_CHANGE))
106     {
107         // Failure or skipped. Don't update the file.
108         goto end;
109     }
110 
111     /* If some edits are to be saved, but we are not making changes to
112      * files (dry-run), just log the fact (MakingChanges() does that). */
113     if ((ec != NULL) && (ec->num_edits > 0) &&
114         !CompareToFile(ctx, ec->file_start, ec->changes_filename, a, pp, result) &&
115         !MakingChanges(ctx, pp, a, result, "edit file '%s'", ec->filename))
116     {
117         goto end;
118     }
119     else if (ec && (ec->num_edits > 0))
120     {
121         if (a->haveeditline || a->edit_template || a->edit_template_string)
122         {
123             if (CompareToFile(ctx, ec->file_start, ec->changes_filename, a, pp, result))
124             {
125                 RecordNoChange(ctx, pp, a, "No edit changes to file '%s' need saving",
126                                ec->filename);
127             }
128             else if (SaveItemListAsFile(ec->file_start, ec->changes_filename, a, ec->new_line_mode))
129             {
130                 RecordChange(ctx, pp, a, "Edited file '%s'", ec->filename);
131                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
132             }
133             else
134             {
135                 RecordFailure(ctx, pp, a, "Unable to save file '%s' after editing", ec->filename);
136                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
137             }
138         }
139 
140         if (a->haveeditxml)
141         {
142 #ifdef HAVE_LIBXML2
143             if (XmlCompareToFile(ec->xmldoc, ec->changes_filename, a->edits))
144             {
145                 if (ec)
146                 {
147                     RecordNoChange(ctx, pp, a, "No edit changes to xml file '%s' need saving",
148                                    ec->filename);
149                 }
150             }
151             else if (SaveXmlDocAsFile(ec->xmldoc, ec->changes_filename, a, ec->new_line_mode))
152             {
153                 RecordChange(ctx, pp, a, "Edited xml file '%s'", ec->filename);
154                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
155             }
156             else
157             {
158                 RecordFailure(ctx, pp, a, "Failed to edit XML file '%s'", ec->filename);
159                 *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
160             }
161             xmlFreeDoc(ec->xmldoc);
162 #else
163             RecordFailure(ctx, pp, a, "Cannot edit XML files without LIBXML2");
164             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
165 #endif
166         }
167     }
168     else if (ec)
169     {
170         RecordNoChange(ctx, pp, a, "No edit changes to file '%s' need saving", ec->filename);
171     }
172 
173 end:
174     if (ec != NULL)
175     {
176         DeleteItemList(ec->file_start);
177         free(ec->changes_filename);
178         free(ec);
179     }
180 }
181 
182 /*********************************************************************/
183 /* Level                                                             */
184 /*********************************************************************/
185 
186 #ifdef HAVE_LIBXML2
187 /***************************************************************************/
188 
LoadFileAsXmlDoc(xmlDocPtr * doc,const char * file,EditDefaults edits)189 bool LoadFileAsXmlDoc(xmlDocPtr *doc, const char *file, EditDefaults edits)
190 {
191     struct stat statbuf;
192 
193     if (stat(file, &statbuf) == -1)
194     {
195         Log(LOG_LEVEL_ERR, "The proposed file '%s' could not be loaded. (stat: %s)", file, GetErrorStr());
196         return false;
197     }
198 
199     if (edits.maxfilesize != 0 && statbuf.st_size > edits.maxfilesize)
200     {
201         Log(LOG_LEVEL_INFO, "File '%s' is bigger than the edit limit. max_file_size = '%jd' > '%d' bytes", file,
202               (intmax_t) statbuf.st_size, edits.maxfilesize);
203         return false;
204     }
205 
206     if (!S_ISREG(statbuf.st_mode))
207     {
208         Log(LOG_LEVEL_INFO, "'%s' is not a plain file", file);
209         return false;
210     }
211 
212     if (statbuf.st_size == 0)
213     {
214         if ((*doc = xmlNewDoc(BAD_CAST "1.0")) == NULL)
215         {
216             Log(LOG_LEVEL_INFO, "Document '%s' not parsed successfully. (xmlNewDoc: %s)", file, GetErrorStr());
217             return false;
218         }
219     }
220     else if ((*doc = xmlParseFile(file)) == NULL)
221     {
222         Log(LOG_LEVEL_INFO, "Document '%s' not parsed successfully. (xmlParseFile: %s)", file, GetErrorStr());
223         return false;
224     }
225 
226     return true;
227 }
228 
229 /*********************************************************************/
230 
SaveXmlCallback(const char * dest_filename,void * param,ARG_UNUSED NewLineMode new_line_mode)231 static bool SaveXmlCallback(const char *dest_filename, void *param,
232                             ARG_UNUSED NewLineMode new_line_mode)
233 {
234     xmlDocPtr doc = param;
235 
236     //saving xml to file
237     if (xmlSaveFile(dest_filename, doc) == -1)
238     {
239         Log(LOG_LEVEL_ERR, "Failed to write xml document to file '%s' after editing. (xmlSaveFile: %s)", dest_filename, GetErrorStr());
240         return false;
241     }
242 
243     return true;
244 }
245 
246 /*********************************************************************/
247 
SaveXmlDocAsFile(xmlDocPtr doc,const char * file,const Attributes * a,NewLineMode new_line_mode)248 bool SaveXmlDocAsFile(xmlDocPtr doc, const char *file, const Attributes *a, NewLineMode new_line_mode)
249 {
250     return SaveAsFile(&SaveXmlCallback, doc, file, a, new_line_mode);
251 }
252 #endif /* HAVE_LIBXML2 */
253