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