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