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