Old package version was not uninstalled on upgrade.
[fa-stable.git] / includes / packages.inc
1 <?php
2 /**********************************************************************
3     Copyright (C) FrontAccounting, LLC.
4         Released under the terms of the GNU General Public License, GPL, 
5         as published by the Free Software Foundation, either version 3 
6         of the License, or (at your option) any later version.
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
10     See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
11 ***********************************************************************/
12 include_once($path_to_root. "/includes/archive.inc");
13 include_once($path_to_root. "/includes/remote_url.inc");
14
15 define('PKG_CACHE_PATH', $path_to_root.'/modules/_cache');
16 define('PUBKEY_PATH', $path_to_root);
17 define('REPO_URL', 'http://'.$repo_auth['login'].':'.$repo_auth['pass'].'@'.$repo_auth['host'].'/'.$repo_auth['branch']);
18 //
19 // FrontAccounting package class
20 //
21 class package extends gzip_file {
22         function package($filename, $basedir=null)
23         {
24                 global $path_to_root;
25
26                 if (!$basedir) {
27                         $basedir = PKG_CACHE_PATH.'/'.substr(basename($filename), 0, -4);
28                         if (file_exists($basedir)) {
29 //                              flush_dir($basedir, true); 
30                         } else
31                         mkdir($basedir);
32                 }
33                 $this->archive($filename);
34                 $this->set_options(array('basedir'=> $basedir));
35                 $this->options['type'] = "pkg";
36         }
37         //
38         //      Used by archive class. Use create_archive() instead.
39         //      
40         function create_pkg() 
41         {
42                 return $this->create_gzip();
43         }
44         //
45         //      Install package and clean temp directory.
46         //
47         function install()
48         {
49                 global $path_to_root;
50                 
51                 $success = true;
52
53                 $this->set_options(array('overwrite' => 1));
54                 $this->extract_files(); // extract package in cache directory
55                 $cachepath = $this->options['basedir'];
56                 $ctrl = get_control_file("$cachepath/_init/config");
57
58                 $targetdir = $path_to_root.'/'.$ctrl['InstallPath'];
59
60                 if (!is_dir($targetdir))
61                         mkdir($targetdir);
62
63                 $dpackage = new package("$cachepath/_data", $targetdir);
64                 $dpackage->set_options(array('overwrite' => 1));
65
66                 $flist = $dpackage->extract_files(true);
67                 if (count($dpackage->error)) {
68                         $this->error = array_merge($this->error, $dpackage->error);
69                         return false;
70                 }
71                 copy_files($flist, $targetdir, "$cachepath/_back");
72         
73                 $dpackage->extract_files(); //install package in target directory
74
75                 $success &= $this->support('install');
76                 $success &= count($dpackage->error) == 0;
77                 $this->error = array_merge($this->error, $dpackage->error);
78                 return $success;
79         }
80         //
81         //      Removing package related sources
82         //
83         function uninstall()
84         {
85                 global $path_to_root;
86
87                 $success = true;
88
89                 $cachepath = $this->options['basedir'];
90                 $ctrl = get_control_file("$cachepath/_init/config");
91
92                 $targetdir = $path_to_root.'/'.$ctrl['InstallPath'];
93
94                 $dpackage = new package("$cachepath/_data", $targetdir);
95
96                 $flist = $dpackage->extract_files(true);
97
98                 $success &= copy_files($flist, "$cachepath/_back", $targetdir, true);
99
100                 $success &= $this->support('uninstall');
101
102                 return $success;
103         }
104         //
105         //      Purge all package related configuration and data.
106         //
107         function purge()
108         {
109                 return true;
110         }
111
112         //
113         //      Call special function defined in package install class
114         //
115         function support($name, $params = null)
116         {
117                 $cachepath = $this->options['basedir'];
118                 if (file_exists("$cachepath/_init/init.php")) {
119                         include("$cachepath/_init/init.php");
120                         if (method_exists($installer, $name)) {
121                                 set_include_path("$cachepath/_init".PATH_SEPARATOR.get_include_path());
122
123                                 $ret = $installer->$name($params);
124                                 return $ret;
125                         }
126                 }
127                 return true;
128         }
129 }
130 //
131 // Changes field value read from control file (single, or multiline) into 
132 // arrays of subfields if needed.
133 //
134 function ufmt_property($key, $value)
135 {
136         // indexes used in output arrays
137         $sub_fields = array(
138                 'MenuTabs' => array('url', 'access', 'tab_id', 'title', 'section'),
139                 'MenuEntries' => array('url', 'access', 'tab_id', 'title'),
140         );
141         if (!isset($sub_fields[$key]))
142                 return $value==='' ? null : $value;
143
144         $prop = array();
145
146         if (!is_array($value))
147                 $value = array($value);
148         foreach($value as $line) {
149                 $indexes = $sub_fields[$key];
150                 $ret = array();
151                 preg_match_all('/(["])(?:\\\\?+.)*?\1|[^"\s][\S]*/', $line, $match);
152                 foreach($match[0] as $n => $subf) {
153                         if ($match[1][$n])
154                                 $val = strtr(substr($subf, 1, -1),
155                                         array('\\"'=>'"'));
156                 else
157                                 $val = $subf;
158                         if (count($indexes))
159                                 $ret[array_shift($indexes)] = $val;
160                         else
161                                 $ret[] = $val;
162                 }
163                 if (count($ret))
164                         $prop[] = $ret;
165         }
166         return $prop;
167 }
168 //=============================================================================
169 //
170 // Retrieve control file and return as associative array
171 //      $index is name of field used as key in result array, or null for numeric keys
172 //
173 function get_control_file($file, $index = false) {
174
175         $list = gzopen($file, 'rb');
176         if (!$list) return null;
177
178         $repo = $pkg = array();
179         $key = false; $value = '';
180         $line = '';
181         do {
182                 $line = rtrim($line);
183                 if ($line && ctype_space($line[0])) { // continuation of multiline property
184                         if (strlen(ltrim($line))) {
185                                 if ($value !== '' && !is_array($value))
186                                         $value = array($value);
187                                 $value[] = ltrim($line);
188                                 continue;
189                         }
190                 }
191                 if ($key) { // save previous property if any
192                         $pkg[$key] = ufmt_property($key, $value);
193                 }
194                 if (!strlen($line)) { // end of section
195                         if (count($pkg)) {
196                                 if ($index !== true) {
197                                         if ($index === false) break;
198                                         if (!isset($pkg[$index])) {
199                                                 display_error(_("No key field '$index' in file '$file'"));
200                                                 return null;
201                                         }
202                                         $repo[$pkg[$index]] = $pkg;
203                                 } else
204                                         $repo[] = $pkg;
205                         }
206                         $pkg = array(); 
207                         $key = null; $value = '';
208                         continue;
209                 } elseif (preg_match('/([^:]*):\s*(.*)/', $line, $m)) {
210                         $key = $m[1]; $value = $m[2];
211                         if (!strlen($key)) {
212                                 display_error("Empty key in line $line");
213                                 return null;
214                         }
215                 } else {
216                         display_error("File parse error in line $line");
217                         return null;
218                 }
219                 
220         } while ((($line = fgets($list))!==false) || $key);
221         fclose($list);
222
223         return $index === false ? $pkg : $repo;
224 }
225 //
226 //      Save configuration data to control file.
227 //
228 function save_control_file($fname, $list, $zip=false) 
229 {
230         $file = $zip ?  gzopen($fname, 'wb') : fopen($fname, 'wb');
231         foreach($list as $section) {
232                 foreach($section as $key => $value) {
233                         if (is_array($value)) { // multiline value
234                                 if (is_array(reset($value))) { // lines have subfields
235                                         foreach($value as $i => $line) {
236                 // Subfields containing white spaces or double quotes are doublequoted 
237                 // with " escaped with backslash.
238                                                 foreach($line as $n => $subfield)
239                                                         if (preg_match('/[\s"]/', $subfield)) {
240                                                                 $value[$i][$n] = 
241                                                                         '"'.strtr($subfield, array('"'=>'\\"')).'"';
242                                                         }
243                                                 // Subfields are separated by white space.
244                                                 $value[$i] = implode(' ', $value[$i]);
245                                         }
246                                 }
247                                 // array elements on subsequent lines starting with white space
248                                 $value = implode("\n ", $value);
249                         }
250                         $zip ? gzwrite($file, "$key: $value\n") : fwrite($file, "$key: $value\n");
251                 }
252                 $zip ? gzwrite($file, "\n"): fwrite($file, "\n");
253         }
254         $zip ? gzclose($file) : fclose($file);
255 }
256 //
257 //      Retrieve text field in localized version or default one 
258 //      when the localized is not avaialable.
259 //
260 function pkg_prop($pkg, $property, $lang=false) 
261 {
262         
263         if ($lang && isset($pkg[$property.'-'.user_language()]))
264                 $prop = @$pkg[$pname];
265         else
266                 $prop = @$pkg[$property];
267
268         return is_array($prop) ? implode("\n ",$prop): $prop;
269 }
270 //
271 //      Retrieve list of packages from repository and return as table ($pkgname==null),
272 //      or find $pkgname package in repository and optionaly download
273 //
274 //      $type is type/s of package
275 //  $filter is optional field selection array in form field=>newkey
276 //              or (0=>field1, 1=>field2...)
277 //  $outkey - field used as a key in package list. If null 'Package' field is used.
278 //
279 function get_pkg_or_list($type = null, $pkgname = null, $filter=array(), $outkey=null, $download=true) {
280
281         global $path_to_root, $repo_auth;
282
283         // first download local copy of repo release file
284         // and check remote signature with local copy of public key
285         //
286         $loclist = PKG_CACHE_PATH.'/Release.gz';
287         
288         if (isset($type) && !is_array($type)) {
289                 $type = array($type);
290         }
291         $refresh = true;
292         do{
293                 if (!file_exists($loclist)) {
294                         url_copy(REPO_URL.'/Release.gz', $loclist);
295                         $refresh = false;
296                 }
297                 $sig = url_get_contents(REPO_URL.'/Release.sig');
298                 $data = file_get_contents($loclist);
299                 $cert = file_get_contents(PUBKEY_PATH.'/FA.pem');
300                 if (!openssl_verify($data, $sig, $cert)) {
301                         if ($refresh)
302                                 @unlink($loclist);
303                         else {
304                                 display_error(_('Release file in repository is invalid, or public key is outdated.'));
305                                 return null;
306                         }
307                 } else
308                         $refresh = false;
309         } while($refresh);
310
311         $Release = get_control_file($loclist, 'Filename');
312         // download and check all indexes containing given package types
313         // then complete package list or seek for pkg
314         $Packages = array();
315         foreach($Release as $fname => $parms) {
316                 if ($type && !count(array_intersect(explode(' ', $parms['Type']), $type))) {
317                         unset($Release[$fname]); continue; // no packages of selected type in this index
318                 }
319                 if ($Release[$fname]['Version'] != $repo_auth['branch']) {
320                         display_warning(_('Repository version does not match application version.')); // ?
321                 }
322                 $remoteindex = REPO_URL.'/'.$fname;
323                 $locindex = PKG_CACHE_PATH.'/'.$fname;
324                 $refresh = true;
325                 do{
326                         if (!file_exists($locindex)) { 
327                                 url_copy($remoteindex, $locindex);
328                                 $refresh = false;
329                         }
330                         if ($parms['SHA1sum'] != sha1_file($locindex)) {        // check subdir index consistency
331                                 if ($refresh)
332                                         @unlink($locindex);
333                                 else {
334                                         display_error(sprintf( _("Security alert: broken index file in repository '%s'. Please inform repository administrator about this issue."),
335                                                 $fname));
336                                         return null;
337                                 }
338                         } else
339                                 $refresh = false;
340                 } while($refresh);
341                 
342                  // scan subdir list and select packages of given type
343                 $pkglist = get_control_file($locindex, 'Package');
344                 foreach($pkglist as $name => $pkg) {
345                         $pkgfullname = REPO_URL.'/'.$parms['Path']."/".$pkg['Filename'].'.pkg';
346                         if (!isset($type) || in_array($pkg['Type'], $type)) {
347                                 if (empty($filter))
348                                         $p = $pkg;
349                                 else {
350                                         foreach($filter as $field => $key) {
351                                                 if (is_numeric($field))
352                                                         $p[$field] = @$pkg[$field];
353                                                 else
354                                                         $p[$key] = @$pkg[$field];
355                                         }
356                                 }
357                                 if ($pkgname == null) {
358                                         $Packages[$outkey ? $outkey : $name] = $p;
359                                 } elseif ($pkgname == $pkg['Package']) {
360                                         //download package to temp directory
361                                         if ($download) {
362                                                 $locname = "$path_to_root/tmp/".$pkg['Filename'].'.pkg';
363                                                 url_copy($pkgfullname, $locname);
364                                                  // checking sha1 hash is expensive proces, so chekc the package
365                                                  // consistency just before downloading
366                                                 if ($pkg['SHA1sum'] != sha1_file($locname)) {
367                                                         display_error(sprintf( _("Security alert: broken package '%s' in repository. Please inform repository administrator about this issue."),
368                                                                 $pkgfullname));
369                                                         return null;
370                                                 }
371                                         }
372                                         return $p;
373                                 }
374                         }
375                 }
376         }
377
378         return $Packages;
379 }
380
381 function get_package($pkgname, $type = null)
382 {
383         return get_pkg_or_list($type, $pkgname);
384 }
385 /*
386         Returns full name of installed package, or null if package is not installed.
387 */
388 function installed_package($package)
389 {
390         $cache = opendir(PKG_CACHE_PATH);
391
392         while ($file = @readdir($cache)) {
393                 if (!is_dir(PKG_CACHE_PATH.'/'.$file))
394                         continue;
395                 if (strpos($file, $package.'-') === 0)
396                         return $file;
397         }
398         @closedir($cache);
399
400         return null;
401 }
402 /*
403         Remove package from system
404 */
405 function uninstall_package($name)
406 {
407         $name = installed_package($name);
408         if (!$name) return true; // not installed
409         $pkg = new package($name.'.pkg');
410         $pkg->uninstall();
411         if($name) {
412                 flush_dir(PKG_CACHE_PATH.'/'.$name, true);
413                 rmdir(PKG_CACHE_PATH.'/'.$name);
414         }
415         return count($pkg->error)==0;
416 }
417
418 //---------------------------------------------------------------------------------------
419 //
420 //      Return merged list of available and installed languages in inform of local 
421 // configuration array supplemented with installed versions information.
422 //
423 function get_languages_list()
424 {
425         global $installed_languages;
426         
427         $pkgs = get_pkg_or_list('language', null, array(
428                                 'Package' => 'package',
429                                 'Version' => 'available',
430                                 'Name' => 'name',
431                                 'Language' => 'code',
432                                 'Encoding' => 'encoding',
433                                 'RTLDir' => 'rtl',
434                                 'Description' => 'Descr',
435                                 'InstallPath' => 'path'
436                         ));
437
438         // add/update languages already installed
439         // 
440         foreach($installed_languages as $id => $l) {
441                 $list = array_search_keys($l['code'], $pkgs, 'code');   // get all packages with this code
442                 foreach ($list as $name) {
443                         if ($l['encoding'] == $pkgs[$name]['encoding']) {       // if the same encoding
444                                 $pkgs[$name]['version'] = @$l['version'];               // set installed version
445                                 $pkgs[$name]['local_id'] = $id;         // index in installed_languages
446                                 continue 2;
447                         }
448                 }
449                 $l['local_id'] = $id;
450                 if (!isset($l['package']) || $l['package'] == '' || !isset($pkgs[$l['package']]))
451                         $pkgs[] = $l;
452                 else
453                         $pkgs[$l['package']] = array_merge($pkgs[$l['package']], $l);
454         }
455         ksort($pkgs);
456         return $pkgs;
457 }
458 //---------------------------------------------------------------------------------------
459 //
460 //      Return merged list of available and installed extensions as a local 
461 // configuration array supplemented with installed versions information.
462 //
463 function get_extensions_list($type = null)
464 {
465
466         if (isset($type) && !is_array($type)) {
467                 $type = array($type);
468         }
469         
470         $pkgs = get_pkg_or_list($type, null, array(
471                                 'Package' => 'package',
472                                 'Version' => 'available',
473                                 'Name' => 'name',
474                                 'Description' => 'Descr',
475                                 'Type' => 'type',
476                                 'DefaultStatus'=> 'active',
477                                 'MenuTabs' => 'tabs',
478                                 'MenuEntries' => 'entries',
479                                 'Encoding' => 'encoding',
480                                 'AccessExtensions' => 'acc_file',
481                                 'InstallPath' => 'path'
482                         ));
483
484         // add/update extensions already installed
485         // 
486         $local = get_company_extensions();
487         foreach($local as $extno => $ext) {
488                 if (!in_array($ext['type'], $type)) continue;
489                 $ext['local_id'] = $extno;
490                 if (!isset($pkgs[$ext['package']]) || $ext['package'] == '')
491                         $pkgs[] = $ext;
492                 else
493                         $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
494         }
495         ksort($pkgs);
496         return $pkgs;
497 }
498 //
499 // Return merged list of available and installed extensions as a local
500 // configuration array supplemented with installed versions information.
501 //
502 function get_themes_list()
503 {
504         $pkgs = get_pkg_or_list('theme', null, array(
505                                 'Package' => 'package',
506                                 'Version' => 'available',
507                                 'Name' => 'name',
508                                 'Description' => 'Descr'
509                         ));
510
511         // add/update extensions already installed
512         // 
513         $local = get_company_extensions();
514         
515         foreach($local as $extno => $ext) {
516                 if (isset($pkgs[@$ext['package']])) {
517                         $ext['local_id'] = $extno;
518                         $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
519                 }
520         }
521         // TODO: Add other themes from themes directory
522         
523         ksort($pkgs);
524         return $pkgs;
525 }
526 //---------------------------------------------------------------------------------------
527 //
528 //      Return merged list of available and installed COAs as a local 
529 // configuration array supplemented with installed versions information.
530 //
531 function get_charts_list()
532 {
533         $pkgs = get_pkg_or_list('chart', null, array(
534                                 'Package' => 'package',
535                                 'Version' => 'available',
536                                 'Name' => 'name',
537                                 'Description' => 'Descr',
538                                 'Type' => 'type',
539                                 'InstallPath' => 'path',
540                                 'Encoding' => 'encoding',
541                                 'SqlScript' => 'sql'
542                         ));
543
544         // add/update default charts
545         // 
546         $local = get_company_extensions();
547
548         foreach($local as $extno => $ext) {
549                 if ($ext['type'] != 'chart') continue;
550                 $ext['local_id'] = $extno;
551                 if (!isset($pkgs[$ext['package']]) || $ext['package'] == '')
552                         $pkgs[] = $ext;
553                 else
554                         $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
555         }
556         ksort($pkgs);
557         return $pkgs;
558 }
559 //---------------------------------------------------------------------------------------------
560 //      Install/update package from repository
561 //
562 function install_language($pkg_name)
563 {
564         global $path_to_root, $installed_languages, $Ajax;
565         
566         $pkg = get_pkg_or_list('language', $pkg_name);
567
568         if ($pkg) {
569                 $i = array_search_key($pkg['Language'], $installed_languages, 'code');
570                 if ($i === null)
571                         $i = count($installed_languages);
572                 else {  // remove another already installed package for this language 
573                         $old_pkg = @$installed_languages[$i]['package'];
574                         if ($old_pkg && ($pkg['Package'] != $old_pkg))
575                                 uninstall_package($old_pkg);
576                 }
577
578                 $package = new package("$path_to_root/tmp/".$pkg['Filename'].'.pkg');
579                 if ($package->install()) {
580                         $lang = array(
581                                 'name' => $pkg['Name'],
582                                 'package' => $pkg['Package'],
583                                 'code' => $pkg['Language'],
584                                 'encoding' => $pkg['Encoding'],
585                                 'version' => $pkg['Version'],
586                                 'path' => $pkg['InstallPath']
587                         );
588                         if ($pkg['RTLDir']=='yes')
589                                 $lang['rtl'] = true;
590                         $installed_languages[$i] = $lang;
591                         write_lang($installed_languages);
592                         unlink("$path_to_root/tmp/".$pkg['Filename'].'.pkg');
593                         $Ajax->activate('lang_tbl');
594                 } else {
595                         display_error(implode('<br>', $package->error));
596                         return false;
597                 }
598         } else {
599                 display_error(sprintf(_("Package '%s' not found."), $pkg_name));
600                 return false;
601         }
602         return true;
603 }
604 //---------------------------------------------------------------------------------------------
605 //      Install/update extension or theme package from repository
606 //
607 function install_extension($pkg_name)
608 {
609         global $path_to_root, $installed_extensions, $next_extension_id, $Ajax;
610         
611         $pkg = get_pkg_or_list(array('extension', 'theme', 'chart'), $pkg_name);
612         if ($pkg) {
613                 $package = new package("$path_to_root/tmp/".$pkg['Filename'].'.pkg');
614                 $local_exts = get_company_extensions();
615                 if ($package->install()) {
616                         $ext_id = array_search_key($pkg['Package'], $local_exts, 'package');
617                         if ($ext_id === null)
618                                 $ext_id = $next_extension_id++;
619                         else {  // remove another already installed package for this language 
620                                 $old_pkg = $installed_extensions[$ext_id]['package'];
621                                 if ($old_pkg)
622                                         uninstall_package($old_pkg);
623                         }
624                         $ext = array(
625                                 'name' => $pkg['Name'],
626                                 'package' => $pkg['Package'],
627                                 'version' => $pkg['Version'],
628                                 'type' => $pkg['Type'],
629                                 'active' => true,
630                                 'path' => $pkg['InstallPath'],
631                         );
632                         if (isset($pkg['MenuTabs']))
633                                 $ext['tabs'] = $pkg['MenuTabs'];
634                         if (isset($pkg['MenuEntries']))
635                                 $ext['entries'] = $pkg['MenuEntries'];
636                         if (isset($pkg['AccessExtensions']))
637                                 $ext['acc_file'] = $pkg['AccessExtensions'];
638                         if (isset($pkg['SqlScript']))
639                                 $ext['sql'] = $pkg['SqlScript'];
640                         $local_exts[$ext_id] = $ext;
641                         $ret = update_extensions($local_exts);
642                         unlink("$path_to_root/tmp/".$pkg['Filename'].'.pkg');
643                         $Ajax->activate('ext_tbl');
644                         return $ret;
645                 } else {
646                         display_error(implode('<br>', $package->error));
647                         return false;
648                 }
649         } else {
650                 display_error(sprintf(_("Package '%s' not found."), $pkg_name));
651                 return false;
652         }
653         return true;
654 }
655 /*
656         Returns true if newer package version is available
657 */
658 function check_pkg_upgrade($current, $available)
659 {
660         preg_match_all('/[\d]+/', $available, $aver);
661         if (!count($aver[0]))
662                 return false;
663         preg_match_all('/[\d]+/', $current, $cver);
664         if (!count($cver[0]))
665                 return true;
666         foreach($aver[0] as $n => $ver)
667                 if ($ver>@$cver[0][$n]) 
668                         return true;
669         return false;
670 }
671
672 //
673 //      Returns package info from index file
674 //
675 function get_package_info($pkg, $type=null, $filter=array(), $outkey=null, $download=true) {
676         return get_pkg_or_list($type, $pkg, $filter, null, false);
677 }
678
679 ?>