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 include_once($path_to_root. "/includes/hooks.inc");
16 define('PKG_CACHE_PATH', $path_to_root.'/modules/_cache');
17 define('PUBKEY_PATH', $path_to_root);
19 // FrontAccounting package class
21 class package extends gzip_file {
23 function __construct($filename, $basedir=null)
28 $basedir = PKG_CACHE_PATH.'/'.substr(basename($filename), 0, -4);
29 if (file_exists($basedir)) {
30 // flush_dir($basedir, true);
34 parent::__construct($filename);
35 $this->set_options(array('basedir'=> $basedir));
36 $this->options['type'] = "pkg";
39 // Used by archive class. Use create_archive() instead.
43 return $this->create_gzip();
46 // Install package and clean temp directory.
54 $this->set_options(array('overwrite' => 1));
55 $this->extract_files(); // extract package in cache directory
56 $cachepath = $this->options['basedir'];
57 $ctrl = get_control_file("$cachepath/_init/config");
59 $targetdir = $path_to_root.'/'.$ctrl['InstallPath'];
61 if (!is_dir($targetdir))
64 $dpackage = new package("$cachepath/_data", $targetdir);
65 $dpackage->set_options(array('overwrite' => 1));
67 $flist = $dpackage->extract_files(true);
68 if (count($dpackage->error)) {
69 $this->error = array_merge($this->error, $dpackage->error);
72 copy_files($flist, $targetdir, "$cachepath/_back");
74 $dpackage->extract_files(); //install package in target directory
76 $install = hook_invoke($ctrl['Package'], 'install_extension', $dummy);
77 $success &= $install===null || $install;
78 $success &= count($dpackage->error) == 0;
79 $this->error = array_merge($this->error, $dpackage->error);
83 // Removing package related sources
91 $cachepath = $this->options['basedir'];
92 $ctrl = get_control_file("$cachepath/_init/config");
94 $targetdir = $path_to_root.'/'.$ctrl['InstallPath'];
96 $dpackage = new package("$cachepath/_data", $targetdir);
98 $flist = $dpackage->extract_files(true);
100 $success &= copy_files($flist, "$cachepath/_back", $targetdir, true);
102 if (strpos($ctrl['InstallPath'], 'modules/') === 0) { // flush module directory
103 flush_dir($targetdir, true);
107 $uninstall = hook_invoke($ctrl['Package'], 'uninstall_extension', $dummy);
108 $success &= $uninstall===null || $uninstall;
113 // Purge all package related configuration and data.
122 // Changes field value read from control file (single, or multiline) into
123 // arrays of subfields if needed.
125 function ufmt_property($key, $value)
127 // indexes used in output arrays
129 // 'MenuTabs' => array('url', 'access', 'tab_id', 'title', 'section'),
130 // 'MenuEntries' => array('url', 'access', 'tab_id', 'title'),
132 if (!isset($sub_fields[$key]))
133 return $value==='' ? null : $value;
137 if (!is_array($value))
138 $value = array($value);
139 foreach($value as $line) {
140 $indexes = $sub_fields[$key];
142 preg_match_all('/(["])(?:\\\\?+.)*?\1|[^"\s][\S]*/', $line, $match);
143 foreach($match[0] as $n => $subf) {
145 $val = strtr(substr($subf, 1, -1),
150 $ret[array_shift($indexes)] = $val;
159 //=============================================================================
161 // Retrieve control file and return as associative array
162 // $index is name of field used as key in result array, or null for numeric keys
164 function get_control_file($file, $index = false) {
166 $list = gzopen($file, 'rb');
167 if (!$list) return null;
169 $repo = $pkg = array();
170 $key = false; $value = '';
173 $line = rtrim($line);
174 if ($line && ctype_space($line[0])) { // continuation of multiline property
175 if (strlen(ltrim($line))) {
176 if ($value !== '' && !is_array($value))
177 $value = array($value);
178 $value[] = ltrim($line);
182 if ($key) { // save previous property if any
183 $pkg[$key] = ufmt_property($key, $value);
185 if (!strlen($line)) { // end of section
187 if ($index !== true) {
188 if ($index === false) break;
189 if (!isset($pkg[$index])) {
190 display_error(sprintf(_("No key field '%s' in file '%s'"), $index, $file));
193 $repo[$pkg[$index]] = $pkg;
198 $key = null; $value = '';
200 } elseif (preg_match('/([^:]*):\s*(.*)/', $line, $m)) {
201 $key = $m[1]; $value = $m[2];
203 display_error("Empty key in line $line");
207 display_error("File parse error in line $line");
211 } while ((($line = fgets($list))!==false) || $key);
214 return $index === false ? $pkg : $repo;
217 // Save configuration data to control file.
219 function save_control_file($fname, $list, $zip=false)
221 $file = $zip ? gzopen($fname, 'wb') : fopen($fname, 'wb');
222 foreach($list as $section) {
223 foreach($section as $key => $value) {
224 if (is_array($value)) { // multiline value
225 if (is_array(reset($value))) { // lines have subfields
226 foreach($value as $i => $line) {
227 // Subfields containing white spaces or double quotes are doublequoted
228 // with " escaped with backslash.
229 foreach($line as $n => $subfield)
230 if (preg_match('/[\s"]/', $subfield)) {
232 '"'.strtr($subfield, array('"'=>'\\"')).'"';
234 // Subfields are separated by white space.
235 $value[$i] = implode(' ', $value[$i]);
238 // array elements on subsequent lines starting with white space
239 $value = implode("\n ", $value);
241 $zip ? gzwrite($file, "$key: $value\n") : fwrite($file, "$key: $value\n");
243 $zip ? gzwrite($file, "\n"): fwrite($file, "\n");
245 $zip ? gzclose($file) : fclose($file);
248 // Retrieve text field in localized version or default one
249 // when the localized is not avaialable.
251 function pkg_prop($pkg, $property, $lang=false)
254 if ($lang && isset($pkg[$property.'-'.user_language()]))
255 $prop = @$pkg[$pname];
257 $prop = @$pkg[$property];
259 return is_array($prop) ? implode("\n ",$prop): $prop;
262 // Retrieve list of packages from repository and return as table ($pkgname==null),
263 // or find $pkgname package in repository and optionaly download
265 // $type is type/s of package
266 // $filter is optional field selection array in form field=>newkey
267 // or (0=>field1, 1=>field2...)
268 // $outkey - field used as a key in package list. If null 'Package' field is used.
270 function get_pkg_or_list($type = null, $pkgname = null, $filter=array(), $outkey=null, $download=true) {
272 global $path_to_root, $repo_auth;
274 $repo = (isset($repo_auth['scheme']) ? $repo_auth['scheme'] : 'http://')
275 .(isset($repo_auth['login']) ? $repo_auth['login'].':' : '')
276 .(isset($repo_auth['pass']) ? $repo_auth['pass'].'@' : '')
277 .(isset($repo_auth['host']) ? $repo_auth['host'].'/' : '')
278 .(isset($repo_auth['path']) ? $repo_auth['path'].'/' : '')
279 .$repo_auth['branch'];
281 // first download local copy of repo release file
282 // and check remote signature with local copy of public key
284 $loclist = PKG_CACHE_PATH.'/Release.gz';
285 $target_dir = $download==true ? VARLIB_PATH."/" : $download;
287 if (isset($type) && !is_array($type)) {
288 $type = array($type);
292 if (!file_exists($loclist)) {
293 if (!url_copy($repo.'/Release.gz', $loclist))
295 display_error(_("Cannot download repo index file." ));
300 $sig = url_get_contents($repo.'/Release.sig');
301 $data = file_get_contents($loclist);
302 $cert = file_get_contents(PUBKEY_PATH.'/FA.pem');
303 if (!function_exists('openssl_verify')) {
304 display_error(_("OpenSSL have to be available on your server to use extension repository system."));
307 if (openssl_verify($data, $sig, $cert) <= 0) {
309 if (!@unlink($loclist))
311 display_error(sprintf(_("Cannot delete outdated '%s' file."), $loclist));
315 display_error(_('Release file in repository is invalid, or public key is outdated.'));
323 $Release = get_control_file($loclist, 'Filename');
324 // download and check all indexes containing given package types
325 // then complete package list or seek for pkg
327 foreach($Release as $fname => $parms) {
328 if ($type && !count(array_intersect(explode(' ', $parms['Type']), $type))) {
329 unset($Release[$fname]); continue; // no packages of selected type in this index
331 if ($Release[$fname]['Version'] != $repo_auth['branch']) {
332 display_warning(_('Repository version does not match application version.')); // ?
334 $remoteindex = $repo.'/'.$fname;
335 $locindex = PKG_CACHE_PATH.'/'.$fname;
338 if (!file_exists($locindex)) {
339 if (!url_copy($remoteindex, $locindex)) {
340 display_error(sprintf(_("Cannot download '%s' file." ), $fname));
345 if ($parms['SHA1sum'] != sha1_file($locindex)) { // check subdir index consistency
347 if (!@unlink($locindex)) {
348 display_error(sprintf(_("Cannot delete outdated '%s' file."), $locindex));
352 display_error(sprintf( _("Security alert: broken index file in repository '%s'. Please inform repository administrator about this issue."),
360 // scan subdir list and select packages of given type
361 $pkglist = get_control_file($locindex, 'Package');
362 foreach($pkglist as $name => $pkg) {
363 $pkgfullname = $repo.'/'.$parms['Path']."/".$pkg['Filename'].'.pkg';
364 if (!isset($type) || in_array($pkg['Type'], $type)) {
368 foreach($filter as $field => $key) {
369 if (is_numeric($field))
370 $p[$field] = @$pkg[$field];
372 $p[$key] = @$pkg[$field];
375 if ($pkgname == null) {
376 $Packages[$outkey ? $outkey : $name] = $p;
377 } elseif ($pkgname == $pkg['Package']) {
378 //download package to temp directory
380 $locname = $target_dir.$pkg['Filename'].'.pkg';
381 if (!url_copy($pkgfullname, $locname)) {
382 display_error(sprintf(_("Cannot download '%s' file." ), $pkgfullname));
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 return get_pkg_or_list($type, $pkgname);
407 Returns full name of installed package, or null if package is not installed.
409 function installed_package($package)
411 $cache = opendir(PKG_CACHE_PATH);
413 while ($file = @readdir($cache)) {
414 if (!is_dir(PKG_CACHE_PATH.'/'.$file))
416 if (strpos($file, $package.'-') === 0)
424 Remove package from system
426 function uninstall_package($name)
428 $name = installed_package($name);
429 if (!$name) return true; // not installed
430 $pkg = new package($name.'.pkg');
433 flush_dir(PKG_CACHE_PATH.'/'.$name, true);
434 rmdir(PKG_CACHE_PATH.'/'.$name);
436 return count($pkg->error)==0;
439 //---------------------------------------------------------------------------------------
441 // Return merged list of available and installed languages in inform of local
442 // configuration array supplemented with installed versions information.
444 function get_languages_list()
446 global $installed_languages;
448 $pkgs = get_pkg_or_list('language', null, array(
449 'Package' => 'package',
450 'Version' => 'available',
452 'Language' => 'code',
453 'Encoding' => 'encoding',
455 'Description' => 'Descr',
456 'InstallPath' => 'path'
459 // add/update languages already installed
461 foreach($installed_languages as $id => $l) {
462 $list = array_search_keys($l['code'], $pkgs, 'code'); // get all packages with this code
463 foreach ($list as $name) {
464 if ($l['encoding'] == $pkgs[$name]['encoding']) { // if the same encoding
465 $pkgs[$name]['version'] = @$l['version']; // set installed version
466 $pkgs[$name]['local_id'] = $id; // index in installed_languages
470 $l['local_id'] = $id;
471 if (!isset($l['package']) || $l['package'] == '' || !isset($pkgs[$l['package']]))
474 $pkgs[$l['package']] = array_merge($pkgs[$l['package']], $l);
480 //---------------------------------------------------------------------------------------
482 // Return merged list of available and installed extensions as a local
483 // configuration array supplemented with installed versions information.
485 function get_extensions_list($type = null)
487 global $path_to_root;
489 if (isset($type) || !is_array($type)) {
490 $type = array($type);
493 $pkgs = get_pkg_or_list($type, null, array(
494 'Package' => 'package',
495 'Version' => 'available',
497 'Description' => 'Descr',
499 'DefaultStatus' => 'active',
500 // 'MenuTabs' => 'tabs',
501 // 'MenuEntries' => 'entries',
502 'Encoding' => 'encoding',
503 // 'AccessExtensions' => 'acc_file',
504 'InstallPath' => 'path'
507 // lookup for local extensions
508 $path = $path_to_root.'/modules/';
510 $moddir = opendir($path);
512 while(false != ($fname = readdir($moddir)))
514 if(!in_array($fname, array('.','..','CVS','_cache')) && is_dir($path.$fname))
516 if (!isset($pkgs[$fname]))
517 $pkgs[$fname] = array(
522 'type' => 'extension',
523 'path' => 'modules/'.$fname,
529 // add/update extensions already installed
531 $installed = get_company_extensions();
532 foreach($installed as $extno => $ext) {
533 if (!in_array($ext['type'], $type)) continue;
534 $ext['local_id'] = $extno;
535 // if (!isset($pkgs[$ext['package']]) || $ext['package'] == '')
538 $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
545 // Return merged list of available and installed extensions as a local
546 // configuration array supplemented with installed versions information.
548 function get_themes_list()
550 $pkgs = get_pkg_or_list('theme', null, array(
551 'Package' => 'package',
552 'Version' => 'available',
554 'Description' => 'Descr'
557 // add/update extensions already installed
559 $local = get_company_extensions();
561 foreach($local as $extno => $ext) {
562 if (isset($pkgs[@$ext['package']])) {
563 $ext['local_id'] = $extno;
564 $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
567 // TODO: Add other themes from themes directory
572 //---------------------------------------------------------------------------------------
574 // Return merged list of available and installed COAs as a local
575 // configuration array supplemented with installed versions information.
577 function get_charts_list()
579 $pkgs = get_pkg_or_list('chart', null, array(
580 'Package' => 'package',
581 'Version' => 'available',
583 'Description' => 'Descr',
585 'InstallPath' => 'path',
586 'Encoding' => 'encoding',
590 // add/update default charts
592 $local = get_company_extensions();
594 foreach($local as $extno => $ext) {
595 if ($ext['type'] != 'chart') continue;
596 $ext['local_id'] = $extno;
597 if (!isset($pkgs[$ext['package']]) || $ext['package'] == '')
600 $pkgs[$ext['package']] = array_merge($pkgs[$ext['package']], $ext);
606 //---------------------------------------------------------------------------------------------
607 // Install/update package from repository
609 function install_language($pkg_name)
611 global $path_to_root, $installed_languages, $Ajax;
613 $pkg = get_pkg_or_list('language', $pkg_name);
616 $i = array_search_key($pkg['Language'], $installed_languages, 'code');
618 $i = count($installed_languages);
619 else { // remove another already installed package for this language
620 $old_pkg = @$installed_languages[$i]['package'];
621 if ($old_pkg && ($pkg['Package'] != $old_pkg))
622 uninstall_package($old_pkg);
625 $package = new package(VARLIB_PATH."/".$pkg['Filename'].'.pkg');
626 if ($package->install()) {
628 'name' => $pkg['Name'],
629 'package' => $pkg['Package'],
630 'code' => $pkg['Language'],
631 'encoding' => $pkg['Encoding'],
632 'version' => $pkg['Version'],
633 'path' => $pkg['InstallPath']
635 if ($pkg['RTLDir']=='yes')
637 $installed_languages[$i] = $lang;
638 write_lang($installed_languages);
639 unlink(VARLIB_PATH."/".$pkg['Filename'].'.pkg');
640 $Ajax->activate('lang_tbl');
642 display_error(implode('<br>', $package->error));
646 display_error(sprintf(_("Package '%s' not found."), $pkg_name));
651 //---------------------------------------------------------------------------------------------
652 // Install/update extension or theme package from repository
654 function install_extension($pkg_name)
656 global $path_to_root, $installed_extensions, $next_extension_id, $Ajax, $db_connections;
658 $pkg = get_pkg_or_list(array('extension', 'theme', 'chart'), $pkg_name);
660 $package = new package(VARLIB_PATH."/".$pkg['Filename'].'.pkg');
661 $local_exts = get_company_extensions();
662 if ($package->install()) {
663 $ext_id = array_search_key($pkg['Package'], $local_exts, 'package');
664 if ($ext_id === null)
665 $ext_id = $next_extension_id++;
666 else { // remove another already installed package for this language
667 $old_pkg = $installed_extensions[$ext_id]['package'];
669 uninstall_package($old_pkg);
672 'name' => $pkg['Name'],
673 'package' => $pkg['Package'],
674 'version' => $pkg['Version'],
675 'type' => $pkg['Type'],
676 'active' => @$pkg['DefaultStatus'] == 'active' ? true : false,
677 'path' => $pkg['InstallPath'],
679 if (isset($pkg['SqlScript']))
680 $ext['sql'] = $pkg['SqlScript'];
682 $local_exts[$ext_id] = $ext;
683 $ret = update_extensions($local_exts);
685 if (($ext['active'] == true) && file_exists($path_to_root.'/'.$ext['path'].'/hooks.php'))
687 // we need to include the new hooks file to activate extension
688 include_once($path_to_root.'/'.$ext['path'].'/hooks.php');
689 foreach($db_connections as $comp => $db)
690 activate_hooks($ext['package'], $comp);
693 unlink(VARLIB_PATH."/".$pkg['Filename'].'.pkg');
694 $Ajax->activate('ext_tbl');
697 display_error(implode('<br>', $package->error));
701 display_error(sprintf(_("Package '%s' not found."), $pkg_name));
707 Returns true if newer package version is available
709 function check_pkg_upgrade($current, $available)
711 if ($available == NULL)
713 preg_match_all('/[\d]+/', $available, $aver);
714 if (!count($aver[0]))
716 if ($current == NULL)
718 preg_match_all('/[\d]+/', $current, $cver);
719 if (!count($cver[0]))
721 foreach($aver[0] as $n => $ver)
722 if ($ver>@$cver[0][$n])
728 // Returns package info from index file
730 function get_package_info($pkg, $type=null, $filter=array(), $outkey=null, $download=true) {
731 return get_pkg_or_list($type, $pkg, $filter, null, false);
735 Check basic extension source compatibility.
737 function check_src_ext_version($ext_v)
741 $compat_levels = 2; // current policy is keeping compatibility on major version level.
742 $app = explode('.', substr($src_version, 0, strspn($src_version, "0123456789.")));
743 $pkg = explode('.', substr($ext_v, 0, strspn($ext_v, "0123456789.")));
745 for ($i=0; $i < min($compat_levels, count($app)); $i++)
746 if ($pkg[$i] < $app[$i])