Handle hostnames with upper-case letters
[webmin.git] / software / msi-lib.pl
1 #) msi-lib.pl
2 # Functions for Windows package management
3
4 $msi_package_logdir = "$module_config_directory/msis";
5 $sw_discovery_cmd = "$root_directory/SW_Discovery/SW_Discovery.exe";
6 $has_sw_discovery = -x $sw_discovery_cmd;
7 $no_package_filesearch = 1;
8
9 sub list_package_system_commands
10 {
11 return ($sw_discovery_cmd);
12 }
13
14 # list_packages([package]*)
15 # Fills the array %packages with all or listed packages
16 sub list_packages
17 {
18 %packages = ( );
19 if ($has_sw_discovery) {
20         local %want = map { $_, 1 } @_;
21         open(OUT, "$sw_discovery_cmd |");
22         local $n = -1;
23         while(<OUT>) {
24                 s/\r|\n//g;
25                 if (/ID\s*=\s*\{(\S+)\}\s*(\S*)/ ||
26                     /ID\s*=\s*(.*)/) {
27                         $n++;
28                         $packages{$n,'name'} = $1;
29                         $packages{$n,'arch'} = 'x86';
30                         $packages{$n,'version'} = $2;
31                         if (/ID\s*=\s*\{(\S+)\}/) {
32                                 $packages{$n,'code'} = $1;
33                                 }
34                         else {
35                                 $packages{$n,'nouninstall'} = 1;
36                                 }
37                         $packages{$n,'nolist'} = 1;
38                         }
39                 elsif (/Package\s+Name\s*:\s*(.*)/) {
40                         if ($packages{$n,'code'}) {
41                                 $packages{$n,'name'} = $1;
42                                 }
43                         else {
44                                 $packages{$n,'desc'} = $1;
45                                 }
46                         }
47                 elsif (/Version\s*:\s*(.*)/) {
48                         $packages{$n,'version'} = $1;
49                         }
50                 elsif (/Publisher\s*:\s*(.*)/) {
51                         $packages{$n,'vendor'} = $1;
52                         $packages{$n,'class'} = $1;
53                         $packages{$n,'class'} =~ s/(http|https):\/\///g;
54                         }
55                 elsif (/Location\s*:\s*(\S.*)/) {
56                         $packages{$n,'desc'} ||= $1;
57                         local @st = stat($1);
58                         $packages{$n,'installed'} = $st[9];
59                         }
60                 }
61         close(OUT);
62         $n++;
63
64         # Filter out un-wanted packages, or those with no name
65         for(my $i=0; $i<$n; $i++) {
66                 if (%want && !$want{$packages{$i,'name'}} ||
67                     !$packages{$i,'name'}) {
68                         if ($i != $n-1) {
69                                 foreach my $f ('name', 'code', 'arch',
70                                                'version', 'class',
71                                                'vendor', 'installed') {
72                                         $packages{$i,$f} = 
73                                                 $packages{$i+1,$f};
74                                         }
75                                 }
76                         $i--;
77                         $n--;
78                         }
79                 }
80
81         return $n;
82         }
83
84 # Query built-in list of those we know were installed
85 local @pkgs = @_;
86 @pkgs = &list_msi_packages() if (!@pkgs);
87 local $i = 0;
88 foreach my $p (@pkgs) {
89         local $msi = &get_msi_package($p);
90         $packages{$i,'name'} = $msi->{'name'};
91         $packages{$i,'version'} = $msi->{'version'};
92         $packages{$i,'desc'} = $msi->{'desc'};
93         $packages{$i,'nolist'} = 0;
94         $i++;
95         }
96 return $i;
97 }
98
99 # list_msi_packages()
100 # Returns the names of installed MSI packages (using Webmin)
101 sub list_msi_packages
102 {
103 opendir(DIR, $msi_package_logdir);
104 local @rv = grep { $_ ne "." && $_ ne ".." } readdir(DIR);
105 closedir(DIR);
106 return @rv;
107 }
108
109 # get_msi_package(name)
110 # Returns a hash containing details of an MSI package
111 sub get_msi_package
112 {
113 local ($pkg) = @_;
114 local %msi;
115 &read_file("$msi_package_logdir/".lc($pkg), \%msi);
116 $msi{'name'} = $pkg;
117 return \%msi;
118 }
119
120 # save_msi_package(&package)
121 # Updates the local details of an MSI package
122 sub save_msi_package
123 {
124 local ($msi) = @_;
125 &make_dir($msi_package_logdir, 0700);
126 &write_file("$msi_package_logdir/".lc($msi->{'name'}), $msi);
127 }
128
129 # delete_msi_package(name)
130 # Removes the local details of an MSI package
131 sub delete_msi_package
132 {
133 local ($name) = @_;
134 unlink("$msi_package_logdir/".lc($name));
135 }
136
137 # package_info(package, [version])
138 # Returns an array of package information in the order
139 #  name, class, description, arch, version, vendor, installtime,
140 #  no-uninstall, no-list
141 sub package_info
142 {
143 local ($name, $ver) = @_;
144 if ($has_sw_discovery) {
145         # Get from total list
146         local %packages;
147         local $n = &list_packages();
148         for(my $i=0; $i<$n; $i++) {
149                 if ($packages{$i,'name'} eq $name &&
150                     (!$ver || $ver eq $packages{$i,'version'})) {
151                         return ($packages{$i,'name'},
152                                 $packages{$i,'vendor'},
153                                 $packages{$i,'desc'},
154                                 $packages{$i,'arch'},
155                                 $packages{$i,'version'},
156                                 $packages{$i,'vendor'},
157                                 $packages{$i,'installed'} ?
158                                    &make_date($packages{$i,'installed'}) :
159                                    "Unknown",
160                                 $packages{$i,'nouninstall'},
161                                 $packages{$i,'nolist'},
162                                 );
163                         }
164                 }
165         return ( );
166         }
167 else {
168         # Query our savedd DB
169         local $msi = &get_msi_package($name);
170         return ( $msi->{'name'}, $msi->{'class'}, $msi->{'desc'},
171                  $msi->{'arch'}, $msi->{'version'}, $msi->{'vendor'},
172                  &make_date($msi->{'installed'}),
173                  $msi->{'code'} ? 1 : 0,
174                  0 );
175         }
176 }
177
178 # is_package(file)
179 # Check if some file is a package file
180 sub is_package
181 {
182 local ($file) = @_;
183 return $file =~ /\.msi$/i ? 1 : 0;
184 }
185
186 # file_packages(file)
187 # Returns a list of all packages in the given file, in the form
188 #  package-version description
189 # XXX how to get proper description?
190 sub file_packages
191 {
192 local ($file) = @_;
193 if ($file =~ /([^\/\\]+)\.msi$/i) {
194         local $suffix = $1;
195         $suffix =~ s/_\d.*$//;  # For files named like apache_2.x.y...
196         return $suffix;
197         }
198 return ( );
199 }
200
201 # install_options(file, package)
202 # Outputs HTML for choosing install options for some package
203 sub install_options
204 {
205 local ($file, $pkg) = @_;
206 print &ui_table_row($text{'msi_users'},
207              &ui_radio("users", 2, [ [ 0, $text{'msi_users0'} ],
208                                      [ 1, $text{'msi_users1'} ],
209                                      [ 2, $text{'msi_users2'} ] ]), 3);
210 }
211
212 # install_package(file, package, [&inputs])
213 # Install the given package from the given file, using options from %in. Returns
214 # undef on success or an error message on failure.
215 sub install_package
216 {
217 local ($file, $pkg, $in) = @_;
218
219 # Run the msiexec command
220 local $temp = &tempname();
221 $file =~ s/\//\\/g;
222 $temp =~ s/\//\\/g;
223 local @st = stat($file);
224 system("msiexec /i ".&quote_path($file)." /quiet /norestart ".
225        "/l*vx ".&quote_path($temp)." >$null_file");
226 &wait_till_stopped_logging($temp);
227
228 # Read output from log file
229 local $ok;
230 local %msi = ( 'name' => $pkg,
231                'arch' => 'x86',                 # Wrong!
232                'installed' => time() );
233 local $fc = 0;
234 open(OUT, $temp);
235 while(<OUT>) {
236         s/\r|\n//g;
237         s/\0//g;                # Strip unicode, primitively
238         if (/Product:\s*(.*)\s\-\-.*success/i) {
239                 $ok = 1;
240                 }
241         elsif (/ProductVersion\s*=\s*(.*)/) {
242                 $msi{'version'} = $1;
243                 }
244         elsif (/ProductName\s*=\s*(.*)/) {
245                 $msi{'desc'} = $1;
246                 }
247         elsif (/ProductCode\s*=\s*(.*)/) {
248                 $msi{'code'} = $1;
249                 }
250         elsif (/Manufacturer\s*=\s*(.*)/) {
251                 $msi{'vendor'} = $1;
252                 }
253         elsif (/Note:\s+\d+:\s+\d+\s+\d+:\s+([a-z]:.*)/) {
254                 $msi{'files_'.$fc} = $1;
255                 local @st = stat($1);
256                 $msi{'sizes_'.$fc} = $st[7];
257                 $fc++;
258                 }
259         }
260 close(OUT);
261 if (!$ok) {
262         return "MSI install failed!";
263         }
264 &save_msi_package(\%msi);
265
266 # Make available to users, if requested
267 if ($in->{'users'}) {
268         local $flag = $in->{'users'} == 1 ? "u" : "m";
269         system("msiexec /j$flag ".&quote_path($file)." /quiet /norestart ".
270                ">$null_file");
271         }
272
273 return undef;
274 }
275
276 # check_files(package, version)
277 # Fills in the %files array with information about the files belonging
278 # to some package. Values in %files are  path type user group size error
279 sub check_files
280 {
281 local ($name, $ver) = @_;
282 local $msi = &get_msi_package($name);
283 local $i;
284 for($i=0; defined($msi->{'files_'.$i}); $i++) {
285         $files{$i,'path'} = $msi->{'files_'.$i};
286         local @st = stat($files{$i,'path'});
287         $files{$i,'type'} = -d $files{$i,'path'} ? 1 : 0;
288         $files{$i,'size'} = $msi->{'sizes_'.$i};
289         if (!@st) {
290                 $files{$i,'error'} = $text{'msi_missing'};
291                 }
292         elsif ($files{$i,'size'} ne $st[7]) {
293                 $files{$i,'error'} = $text{'msi_size'};
294                 }
295         }
296 return $i;
297 }
298
299 # delete_options(package)
300 # Outputs HTML for package uninstall options
301 sub delete_options
302 {
303 local ($name) = @_;
304 # None!
305 }
306
307 # delete_package(package, [&options], version)
308 # Attempt to remove some package
309 sub delete_package
310 {
311 local ($name, $opts, $ver) = @_;
312
313 # Call the uninstall command
314 local $code;
315 if ($has_sw_discovery) {
316         # Get code form package list
317         local %packages;
318         local $n = &list_packages();
319         for(my $i=0; $i<$n; $i++) {
320                 if ($packages{$i,'name'} eq $name &&
321                     !$ver || $ver eq $packages{$i,'version'}) {
322                         $code = $packages{$i,'code'};
323                         }
324                 }
325         $code || &error("Couldn't find install code for $name");
326         }
327 else {
328         local $msi = &get_msi_package($name);
329         $code = $msi->{'code'};
330         }
331 local $temp = &tempname();
332 $temp =~ s/\//\\/g;
333 system("msiexec /x \"$code\" /quiet /norestart ".
334        "/l*vx ".&quote_path($temp)." >$null_file");
335 &wait_till_stopped_logging($temp);
336
337 # Check log for success
338 local $ok = 0;
339 open(LOG, $temp);
340 while(<LOG>) {
341         s/\r|\n//g;
342         s/\0//g;                # Strip unicode, primitively
343         if (/Product:\s*(.*)\s\-\-.*success/i) {
344                 $ok = 1;
345                 }
346         }
347 close(LOG);
348 if (!$ok) {
349         return "MSI uninstall failed";
350         }
351
352 # Remove from local info
353 &delete_msi_package($name);
354
355 return undef;
356 }
357
358 sub package_system
359 {
360 return "MSI";
361 }
362
363 sub package_help
364 {
365 return "msi";
366 }
367
368 # wait_till_stopped_logging(file)
369 # Spin until some file has remained untouched for 5 seconds
370 sub wait_till_stopped_logging
371 {
372 local ($file) = @_;
373 while(1) {
374         local @before = stat($file);
375         sleep(5);
376         local @after = stat($file);
377         last if ($before[9] == $after[9]);
378         }
379 }
380
381 1;
382