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");
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");
18 $pkg_data_basedir = array(
19 'language' => '/lang/',
21 'extension' => '/modules/',
22 // 'plugin' => '/modules/',
23 'theme' => '/themes/');
26 // FrontAccounting package class
28 class package extends gzip_file {
29 function package($filename, $basedir=null)
34 $basedir = PKG_CACHE_PATH.'/'.substr(basename($filename), 0, -4);
35 if (file_exists($basedir)) {
36 // flush_dir($basedir, true);
40 $this->archive($filename);
41 $this->set_options(array('basedir'=> $basedir));
42 $this->options['type'] = "pkg";
45 // Used by archive class. Use create_archive() instead.
49 return $this->create_gzip();
52 // Install package and clean temp directory.
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");
65 // $targetdir = $pkg_data_basedir[$ctrl['Type']];
67 // $targetdir = $path_to_root.$targetdir.$ctrl['Package'];
69 // $targetdir = $path_to_root;
70 $targetdir = $path_to_root.'/'.$ctrl['InstallPath'];
72 if (!is_dir($targetdir))
75 $dpackage = new package("$cachepath/_data", $targetdir);
76 $dpackage->set_options(array('overwrite' => 1));
78 $flist = $dpackage->extract_files(true);
79 if (count($dpackage->error)) {
80 $this->error = array_merge($this->error, $dpackage->error);
83 copy_files($flist, $targetdir, "$cachepath/_back");
85 $dpackage->extract_files(); //install package in target directory
87 $success &= $this->support('install');
88 $success &= count($dpackage->error) == 0;
89 $this->error = array_merge($this->error, $dpackage->error);
93 // Removing package related sources
101 $cachepath = $this->options['basedir'];
102 $ctrl = get_control_file("$cachepath/_init/config");
103 // $targetdir = $pkg_data_basedir[$ctrl['Type']];
105 // $targetdir = $path_to_root.$targetdir.$ctrl['Package'];
107 // $targetdir = $path_to_root;
108 $targetdir = $path_to_root.'/'.$ctrl['InstallPath'];
110 $dpackage = new package("$cachepath/_data", $targetdir);
112 $flist = $dpackage->extract_files(true);
114 $success &= copy_files($flist, "$cachepath/_back", $targetdir, true);
116 $success &= $this->support('uninstall');
121 // Purge all package related configuration and data.
129 // Call special function defined in package install class
131 function support($name, $params = null)
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());
139 // chdir("$cachepath/_init");
140 $ret = $installer->$name($params);
149 // Changes field value read from control file (single, or multiline) into
150 // arrays of subfields if needed.
152 function ufmt_property($key, $value)
154 // indexes used in output arrays
156 'MenuTabs' => array('url', 'access', 'tab_id', 'title', 'section'),
157 'MenuEntries' => array('url', 'access', 'tab_id', 'title'),
159 if (!isset($sub_fields[$key]))
160 return $value==='' ? null : $value;
164 if (!is_array($value))
165 $value = array($value);
166 foreach($value as $line) {
167 $indexes = $sub_fields[$key];
169 preg_match_all('/(["])(?:\\\\?+.)*?\1|[^"\s][\S]*/', $line, $match);
170 foreach($match[0] as $n => $subf) {
172 $val = strtr(substr($subf, 1, -1),
177 $ret[array_shift($indexes)] = $val;
186 //=============================================================================
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
191 function get_control_file($file, $index = false) {
193 $list = gzopen($file, 'rb');
194 if (!$list) return null;
196 $repo = $pkg = array();
197 $key = false; $value = '';
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);
209 if ($key) { // save previous property if any
210 $pkg[$key] = ufmt_property($key, $value);
212 if (!strlen($line)) { // end of section
214 if ($index !== true) {
215 if ($index === false) break;
216 if (!isset($pkg[$index])) {
217 display_error(_("No key field '$index' in file '$file'"));
220 $repo[$pkg[$index]] = $pkg;
225 $key = null; $value = '';
227 // } elseif (preg_match('/([\w-]*):\s*(.*)/', $line, $m)) {
228 } elseif (preg_match('/([^:]*):\s*(.*)/', $line, $m)) {
229 $key = $m[1]; $value = $m[2];
231 display_error("Empty key in line $line");
235 display_error("File parse error in line $line");
239 } while ((($line = fgets($list))!==false) || $key);
242 return $index === false ? $pkg : $repo;
245 // Save configuration data to control file.
247 function save_control_file($fname, $list, $zip=false)
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)) {
260 '"'.strtr($subfield, array('"'=>'\\"')).'"';
262 // Subfields are separated by white space.
263 $value[$i] = implode(' ', $value[$i]);
266 // array elements on subsequent lines starting with white space
267 $value = //(count($value) ? "\n " :'').
268 implode("\n ", $value);
270 $zip ? gzwrite($file, "$key: $value\n") : fwrite($file, "$key: $value\n");
272 $zip ? gzwrite($file, "\n"): fwrite($file, "\n");
274 $zip ? gzclose($file) : fclose($file);
277 // Retrieve text field in localized version or default one
278 // when the localized is not avaialable.
280 function pkg_prop($pkg, $property, $lang=false)
283 if ($lang && isset($pkg[$property.'-'.user_language()]))
284 $prop = @$pkg[$pname];
286 $prop = @$pkg[$property];
288 return is_array($prop) ? implode("\n ",$prop): $prop;
291 // Retrieve list of packages from repository and return as table ($pkgname==null),
292 // or find $pkgname package in repository and optionaly download
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.
299 function get_pkg_or_list($type = null, $pkgname = null, $filter=array(), $outkey=null, $download=true) {
301 global $path_to_root, $repository, $FA_repo_version;
303 // first download local copy of repo release file
304 // and check remote signature with local copy of public key
306 $loclist = PKG_CACHE_PATH.'/Release.gz';
308 if ($type!=null && !is_array($type)) {
309 $type = array($type);
313 if (!file_exists($loclist)) {
314 copy(REPO_URL.'/Release.gz', $loclist);
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)) {
324 display_error(_('Release file in repository is invalid, or public key is outdated.'));
331 $Release = get_control_file($loclist, 'Filename');
333 // download and check all indexes containing given package types
334 // then complete package list or seek for pkg
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
340 if ($Release[$fname]['Version'] != $FA_repo_version) {
341 display_warning(_('Repository version does not match application version.')); // ?
343 $remoteindex = REPO_URL.'/'.$fname;
344 $locindex = PKG_CACHE_PATH.'/'.$fname;
347 if (!file_exists($locindex)) {
348 copy($remoteindex, $locindex);
351 if ($parms['SHA1sum'] != sha1_file($locindex)) { // check subdir index consistency
355 display_error(sprintf( _("Security alert: broken index file in repository '%s'. Please inform repository administrator about this issue."),
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)) {
371 foreach($filter as $field => $key) {
372 if (is_numeric($field))
373 $p[$field] = @$pkg[$field];
375 $p[$key] = @$pkg[$field];
378 if ($pkgname == null) {
379 $Packages[$outkey ? $outkey : $name] = $p;
380 } elseif ($pkgname == $pkg['Package']) {
381 //download package to temp directory
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."),
402 function get_package($pkgname, $type = null)
404 $all = get_pkg_or_list($type, $pkgname);
405 $pkg = array_search_value($all, $pkgname, 'Package');
408 Returns full name of installed package, or null if package is not installed.
410 function installed_package($package)
412 $cache = opendir(PKG_CACHE_PATH);
414 while ($file = @readdir($cache)) {
415 if (!is_dir(PKG_CACHE_PATH.'/'.$file))
417 if (strpos($file, $package.'-') === 0)
425 Remove package from system
427 function uninstall_package($name)
429 $name = installed_package($name);
430 if (!$name) return true; // not installed
431 $pkg = new package($name.'.pkg');
434 flush_dir(PKG_CACHE_PATH.'/'.$name, true);
435 rmdir(PKG_CACHE_PATH.'/'.$name);
437 return count($pkg->error)==0;
440 //---------------------------------------------------------------------------------------
442 // Return merged list of available and installed languages in inform of local
443 // configuration array supplemented with installed versions information.
445 function get_languages_list()
447 global $installed_languages;
449 $pkgs = get_pkg_or_list('language', null, array(
450 'Package' => 'package',
451 'Version' => 'available',
453 'Language' => 'code',
454 'Encoding' => 'encoding',
456 'Description' => 'Descr',
457 'InstallPath' => 'path'
460 // add/update languages already installed
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
471 $l['local_id'] = $id;
472 if (!isset($pkgs[$l['package']]) || $l['package'] == '')
475 $pkgs[$l['package']] = array_merge($pkgs[$l['package']], $l);
480 //---------------------------------------------------------------------------------------
482 // Return merged list of available and installed extensions in inform of local
483 // configuration array supplemented with installed versions information.
485 function get_extensions_list()
487 $pkgs = get_pkg_or_list('extension', null, array(
488 'Package' => 'package',
489 'Version' => 'available',
491 'Description' => 'Descr',
493 'DefaultStatus'=> 'active',
494 'MenuTabs' => 'tabs',
495 'MenuEntries' => 'entries',
496 'AccessExtensions' => 'acc_file',
497 'InstallPath' => 'path'
500 // add/update extensions already installed
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'] == '')
509 $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
515 // Return merged list of available and installed extensions in inform of local
516 // configuration array supplemented with installed versions information.
518 function get_themes_list()
520 $pkgs = get_pkg_or_list('theme', null, array(
521 'Package' => 'package',
522 'Version' => 'available',
524 'Description' => 'Descr'
527 // add/update extensions already installed
529 $local = get_company_extensions();
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);
537 // TODO: Add other themes from themes directory
542 //---------------------------------------------------------------------------------------------
543 // Install/update package from repository
545 function install_language($pkg_name)
547 global $path_to_root, $installed_languages, $Ajax;
549 $pkg = get_pkg_or_list('language', $pkg_name);
552 $i = array_search_key($pkg['Language'], $installed_languages, 'code');
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);
561 $package = new package("$path_to_root/tmp/".$pkg['Filename'].'.pkg');
562 if ($package->install()) {
564 'name' => $pkg['Name'],
565 'package' => $pkg['Package'],
566 'code' => $pkg['Language'],
567 'encoding' => $pkg['Encoding'],
568 'version' => $pkg['Version'],
569 'path' => $pkg['InstallPath']
571 if ($pkg['RTLDir']=='yes')
573 $installed_languages[$i] = $lang;
574 write_lang($installed_languages);
575 unlink("$path_to_root/tmp/".$pkg['Filename'].'.pkg');
576 $Ajax->activate('lang_tbl');
582 //---------------------------------------------------------------------------------------------
583 // Install/update extension or theme package from repository
585 function install_extension($pkg_name)
587 global $path_to_root, $next_extension_id, $Ajax;
589 $pkg = get_pkg_or_list(array('extension', 'theme'), $pkg_name);
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++;
598 'name' => $pkg['Name'],
599 'package' => $pkg['Package'],
600 'version' => $pkg['Version'],
601 'type' => $pkg['Type'],
603 'path' => $pkg['InstallPath'],
604 // 'tabs' => $pkg['MenuTabs'],
605 // 'entries' => $pkg['MenuEntries'],
606 // 'acc_file' => @$pkg['AccessExtensions'],
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');
619 display_error(implode('<br>', $package->error));
624 Returns true if newer package version is available
626 function check_pkg_upgrade($current, $available)
628 preg_match_all('/[\d]+/', $available, $aver);
629 if (!count($aver[0]))
631 preg_match_all('/[\d]+/', $current, $cver);
632 if (!count($cver[0]))
634 foreach($aver[0] as $n => $ver)
635 if ($ver>@$cver[0][$n])
641 // Returns package info from index file
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);