File Coverage

blib/lib/OpenInteract/Package.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             package OpenInteract::Package;
2              
3             # $Id: Package.pm,v 1.40 2003/01/25 16:16:07 lachoy Exp $
4              
5             # This module manipulates information from individual packages to
6             # perform some action in the package files.
7              
8 1     1   6 use strict;
  1         2  
  1         34  
9              
10 1     1   1227 use Archive::Tar ();
  1         380322  
  1         32  
11 1     1   14 use Cwd qw( cwd );
  1         2  
  1         74  
12 1     1   6 use Data::Dumper qw( Dumper );
  1         2  
  1         49  
13 1     1   3264 use ExtUtils::Manifest ();
  1         17006  
  1         30  
14 1     1   11 use File::Basename ();
  1         2  
  1         20  
15 1     1   5 use File::Copy qw( cp );
  1         2  
  1         63  
16 1     1   5 use File::Path ();
  1         2  
  1         15  
17 1     1   541 use SPOPS::HashFile ();
  0            
  0            
18             use SPOPS::Utility ();
19             require Exporter;
20              
21             @OpenInteract::Package::ISA = qw( Exporter );
22             $OpenInteract::Package::VERSION = sprintf("%d.%02d", q$Revision: 1.40 $ =~ /(\d+)\.(\d+)/);
23             @OpenInteract::Package::EXPORT_OK = qw( READONLY_FILE );
24              
25             use constant READONLY_FILE => '.no_overwrite';
26              
27             # Define the subdirectories present in a package
28              
29             my @PKG_SUBDIR = qw( conf data doc struct template script html html/images );
30              
31             # Fields in our package/configuration
32              
33             my @PKG_FIELDS = qw( name version author url description notes
34             module template_plugin template_block filter
35             base_dir website_dir package_dir website_name
36             dependency script_install script_upgrade
37             script_uninstall sql_installer installed_on
38             installed_by last_updated_on last_updated_by );
39              
40              
41             # Name of the package configuration file, always found in the
42             # package's root directory
43              
44             my $DEFAULT_CONF_FILE = 'package.conf';
45              
46             # Define the keys in 'package.conf' that can be a list, meaning you
47             # can have multiple items defined:
48             #
49             # author Larry Wall
50             # author Chris Winters
51              
52             my %CONF_LIST_KEYS = map { $_ => 1 }
53             qw( author script_install script_upgrade script_uninstall module );
54              
55             # Define the keys in 'package.conf' that can be a hash, meaning that
56             # you can have items defined as multiple key-value pairs
57             # (space-separated):
58             #
59             # dependency base_linked 1.09
60             # dependency static_page 1.18
61              
62             my %CONF_HASH_KEYS = map { $_ => 1 } qw( dependency template_plugin template_block filter );
63              
64             # For exporting a package, the following variables are required in
65             # 'package.conf'
66              
67             my @EXPORT_REQUIRED = qw( name version );
68              
69             # Global for holding Archive::Tar errors
70              
71             my $ARCHIVE_ERROR = undef;
72              
73             # Fields NOT to copy over in conf/spops.perl when creating package in
74             # website from base installation (the first three are ones we
75             # manipulate by hand)
76              
77             my %SPOPS_CONF_KEEP = map { $_ => 1 } qw( class has_a links_to );
78              
79             # These are the default public and site admin group IDs; we use them
80             # when copying over the SPOPS configuration files (see
81             # _copy_spops_config_file())
82              
83             use constant PUBLIC_GROUP_ID => 2;
84             use constant SITE_ADMIN_GROUP_ID => 3;
85              
86             use constant DEBUG => 0;
87              
88              
89             # Create subdirectories for a package.
90              
91             sub create_subdirectories {
92             my ( $class, $dir, $main_class ) = @_;
93             $main_class ||= 'OpenInteract';
94             return undef unless ( -d $dir );
95             foreach my $sub_dir ( @PKG_SUBDIR, $main_class,
96             "$main_class/Handler",
97             "$main_class/SQLInstall" ) {
98             mkdir( "$dir/$sub_dir", 0775 )
99             || die "Cannot create package subdirectory $dir/$sub_dir: $!";
100             }
101             return 1;
102             }
103              
104              
105             # Creates a package directories using our base subdirectories
106             # along with a package.conf file and some other goodies (?)
107              
108             sub create_skeleton {
109             my ( $class, $repository, $name ) = @_;
110             my $pwd = cwd;
111              
112             my $cleaned_pkg = $class->_clean_package_name( $name );
113              
114             # Check directories
115              
116             unless ( $repository ) {
117             die "Cannot create package skeleton: no existing base ",
118             "installation repository specified!\n";
119             }
120              
121             my $base_dir = $repository->{META_INF}{base_dir};
122              
123             if ( -d $cleaned_pkg ) {
124             die "Cannot create package skeleton: directory ($cleaned_pkg) already exists!\n";
125             }
126             mkdir( $cleaned_pkg, 0775 ) || die "Cannot create package directory $cleaned_pkg: $!\n";
127             chdir( $cleaned_pkg );
128              
129             # Then create the subdirectories for the package
130              
131             $class->create_subdirectories( '.' );
132              
133             # This does a replacement so that 'static_page' becomes StaticPage
134              
135             my $uc_first_name = ucfirst $cleaned_pkg;
136             $uc_first_name =~ s/_(\w)/\U$1\U/g;
137              
138             # Copy over files from the samples (located in the base OpenInteract
139             # directory), doing replacements as necessary
140              
141             $class->replace_and_copy({ from_file => "$base_dir/conf/sample-package.conf",
142             to_file => "package.conf",
143             from_text => [ '%%NAME%%', '%%UC_FIRST_NAME%%' ],
144             to_text => [ $cleaned_pkg, $uc_first_name ] });
145              
146             $class->replace_and_copy({ from_file => "$base_dir/conf/sample-package.pod",
147             to_file => "doc/$cleaned_pkg.pod",
148             from_text => [ '%%NAME%%' ],
149             to_text => [ $cleaned_pkg ] });
150              
151             $class->replace_and_copy({ from_file => "$base_dir/conf/sample-doc-titles",
152             to_file => "doc/titles",
153             from_text => [ '%%NAME%%' ],
154             to_text => [ $cleaned_pkg ] });
155              
156             $class->replace_and_copy({ from_file => "$base_dir/conf/sample-SQLInstall.pm",
157             to_file => "OpenInteract/SQLInstall/$uc_first_name.pm",
158             from_text => [ '%%NAME%%', '%%UC_FIRST_NAME%%' ],
159             to_text => [ $cleaned_pkg, $uc_first_name ] });
160              
161             $class->replace_and_copy({ from_file => "$base_dir/conf/sample-Handler.pm",
162             to_file => "OpenInteract/Handler/$uc_first_name.pm",
163             from_text => [ '%%NAME%%', '%%UC_FIRST_NAME%%' ],
164             to_text => [ $cleaned_pkg, $uc_first_name ] });
165              
166             cp( "$base_dir/conf/sample-spops.perl", "conf/spops.perl" )
167             || _w( 0, "Cannot copy sample (conf/spops.perl): $!" );
168             cp( "$base_dir/conf/sample-action.perl", "conf/action.perl" )
169             || _w( 0, "Cannot copy sample (conf/action.perl): $!" );
170             cp( "$base_dir/conf/sample-MANIFEST.SKIP", "MANIFEST.SKIP" )
171             || _w( 0, "Cannot copy sample (MANIFEST.SKIP): $!" );
172             cp( "$base_dir/conf/sample-dummy-template.meta", "template/dummy.meta" )
173             || _w( 0, "Cannot copy sample (template/dummy.meta): $!" );
174             cp( "$base_dir/conf/sample-dummy-template.tmpl", "template/dummy.tmpl" )
175             || _w( 0, "Cannot copy sample (template/dummy.tmpl): $!" );
176              
177             # Create a 'Changes' file
178              
179             eval { open( CHANGES, "> Changes" ) || die $! };
180             if ( $@ ) {
181             _w( 0, "Cannot open 'Changes' file ($!). Please create your ",
182             "own so people can follow your progress." );
183             }
184             else {
185             my $time_stamp = scalar localtime;
186             print CHANGES <
187             Revision history for OpenInteract package $cleaned_pkg.
188              
189             0.01 $time_stamp
190              
191             Package skeleton created by oi_manage
192              
193             INIT
194             close( CHANGES );
195             }
196              
197             # Create a MANIFEST from the pwd
198              
199             $class->_create_manifest();
200              
201             # Go back to the original dir and return the name
202              
203             chdir( $pwd );
204             return $cleaned_pkg;
205             }
206              
207              
208             # Rules for a clean package name:
209             # - Package name cannot have spaces (s/ /_/)
210             # - Package name cannot have dashes (s/-/_/)
211             # - Package name cannot start with a number (die)
212             # - Package name cannot have nonword characters except '_'
213              
214             sub _clean_package_name {
215             my ( $class, $name ) = @_;
216             my ( @clean_actions, @die_actions );
217             $name =~ s/ /_/g && push @clean_actions, "Name must not have spaces";
218             $name =~ s/\-/_/g && push @clean_actions, "Name must not have dashes";
219             $name =~ /^\d/ && push @die_actions, "Name must not start with a number";
220             $name =~ /\W/ && push @die_actions, "Name must not have non-word characters";
221             if ( scalar @die_actions ) {
222             die "Package name unacceptable: \n",
223             join( "\n", @die_actions, @clean_actions ), "\n";
224             }
225             return $name;
226             }
227              
228              
229             # Takes a package file and installs the package to the base
230             # OpenInteract directory.
231              
232             sub install_distribution {
233             my ( $class, $p ) = @_;
234             my $old_pwd = cwd;
235              
236             # ------------------------------
237             # Taken from CGI.pm
238             # FIGURE OUT THE OS WE'RE RUNNING UNDER
239             # Some systems support the $^O variable. If not
240             # available then require() the Config library
241             my $OS = undef;
242             unless ( $OS = $^O ) {
243             require Config;
244             $OS = $Config::Config{'osname'};
245             }
246             # ------------------------------
247              
248             unless ( -f $p->{package_file} ) {
249             die "Package file for installation ($p->{package_file}) does not exist\n";
250             }
251              
252             # TODO: Use File::Spec for this?
253              
254             # Note that this should NOT be just 'win' since 'Darwin' gives a
255             # (very) false positive
256              
257             if ( $OS =~ /Win32/i ) {
258             unless ( $p->{package_file} =~ /^\w:\// ) {
259             $p->{package_file} = join( '/', $old_pwd, $p->{package_file} );
260             }
261             }
262             else {
263             unless ( $p->{package_file} =~ /^\// ) {
264             $p->{package_file} = join( '/', $old_pwd, $p->{package_file} );
265             }
266             }
267             DEBUG && _w( 1, "Package file used for distribution: ($p->{package_file}" );
268              
269             # This is the repository we'll be using
270              
271             my $repos = $p->{repository} ||
272             eval { OpenInteract::PackageRepository->fetch(
273             undef, { directory => $p->{base_dir},
274             perm => 'write' } ) };
275             unless ( $repos ) { die "Cannot open repository: $@\n" }
276             my $base_dir = $repos->{META_INF}{base_dir};
277              
278             my $base_package_file = File::Basename::basename( $p->{package_file} );
279             my ( $package_base ) = $base_package_file =~ /^(.*)\.tar\.gz$/;
280             DEBUG && _w( 1, "Package base: $package_base" );
281              
282             my $rv = $class->_extract_archive( $p->{package_file} );
283             unless ( $rv ) {
284             my $msg = "Error found trying to unpack the distribution! " .
285             "Error: " . $ARCHIVE_ERROR;
286             my $removed_files = $class->_remove_directory_tree( $package_base );
287             die $msg;
288             }
289              
290             # Read in the package config and grab the name/version
291              
292             chdir( $package_base );
293             DEBUG && _w( 1, "Trying to find config file in ($package_base/)" );
294             my $conf_file = $p->{package_conf_file} || $DEFAULT_CONF_FILE;
295             my $conf = $class->read_config({ file => $conf_file });
296             die "No valid package config read!\n" unless ( scalar keys %{ $conf } );
297              
298             my $name = $conf->{name};
299             my $version = $conf->{version};
300             chdir( $old_pwd );
301              
302             # We're all done with the temp stuff, so get rid of it.
303              
304             my $removed_files = $class->_remove_directory_tree( $package_base );
305             DEBUG && _w( 2, "Removed extracted tree, config file found ok." );
306              
307             # Check to see if the package/version already exists
308              
309             my $error_msg = undef;
310             my $exist_info = $repos->fetch_package_by_name({ name => $name,
311             version => $version });
312             if ( $exist_info ) {
313             die "Cannot install since package $name-$version already " .
314             "exists in the base installation repository. (It was installed on " .
315             "$exist_info->{installed_on}).\n\nAborting package installation.\n";
316             }
317             DEBUG && _w( 1, "Package does not currently exist in repository." );
318              
319             # Now see if the package has specified any modules that are
320             # necessary for its operation. For now, we will refuse to install
321             # a package that does not have supporting modules.
322              
323             if ( ref $conf->{module} eq 'ARRAY' ) {
324             my @failed_modules = $class->_check_module_install( @{ $conf->{module} } );
325             if ( scalar @failed_modules ) {
326             die "Package $name-$version requires the following modules " .
327             "that are not currently installed: " . join( ', ', @failed_modules ) .
328             ". Please install them and try again.\n";
329             }
330             }
331              
332             # Create some directory names and move to the base package directory
333             # -- the directory that holds all of the package definitions
334              
335             my $new_pkg_dir = join( '/', 'pkg', "$name-$version" );
336             my $full_pkg_dir = join( '/', $base_dir, $new_pkg_dir );
337             if ( -d $full_pkg_dir ) {
338             die "The directory into which the distribution should be unpacked ",
339             "($full_pkg_dir) already exists. Please remove it and try again.\n";
340             }
341             chdir( join( '/', $base_dir, 'pkg' ) );
342              
343             # Unarchive the package; note that since the archive creates a
344             # directory name-version/blah we don't need to create the directory
345             # ourselves and then chdir() to it.
346              
347             my $extract_rv = $class->_extract_archive( $p->{package_file} );
348             unless ( $extract_rv ) {
349             chdir( $base_dir );
350             $class->_remove_directory_tree( $full_pkg_dir );
351             die "Cannot unpack the distribution into its final " .
352             "directory ($full_pkg_dir)! Error: " . $ARCHIVE_ERROR;
353             }
354             DEBUG && _w( 1, "Unpackaged package into $base_dir/pkg ok" );
355              
356             # Create the package info and try to save; if we're successful, return the
357             # package info.
358              
359             my $info = {
360             base_dir => $base_dir,
361             package_dir => $new_pkg_dir,
362             installed_on => $repos->now };
363             foreach my $conf_field ( keys %{ $conf } ) {
364             $info->{ $conf_field } = $conf->{ $conf_field };
365             }
366             DEBUG && _w( 1, "Trying to save package info: ", Dumper( $info ) );
367              
368             $repos->save_package( $info );
369             eval { $repos->save() };
370             if ( $@ ) {
371             chdir( $base_dir );
372             $class->_remove_directory_tree( $full_pkg_dir );
373             die "Could not save data to installed package database. " .
374             "Error returned: $@ " .
375             "Aborting package installation.";
376             }
377             DEBUG && _w( 1, "Saved repository ok." );
378             chdir( $old_pwd );
379             return $info;
380             }
381              
382              
383             # Install a package from the base OpenInteract directory to a website
384             # directory. This is known in 'oi_manage' terms as 'applying' a
385             # package. Note that if you're upgrading the app calling this module
386             # must first get rid of the old package.
387              
388             sub install_to_website {
389             my ( $class, $base_repository, $website_repository, $info, $CONFIG ) = @_;
390              
391             # Be sure to have the website directory, website name, and package
392             # directory set
393              
394             unless ( $info->{website_name} ) {
395             die "Website name not set in package object.\n";
396             }
397             my $package_name_version = "$info->{name}-$info->{version}";
398             $info->{website_dir} ||= $website_repository->{META_INF}{base_dir};
399             $info->{package_dir} ||= join( '/', 'pkg', $package_name_version );
400              
401             # Then create package directory within the website directory
402              
403             my $pkg_dir = join( '/', $info->{website_dir}, $info->{package_dir} );
404             if ( -d $pkg_dir ) { die "Package directory $pkg_dir already exists.\n" }
405             mkdir( $pkg_dir, 0775 ) || die "Cannot create $pkg_dir : $!";
406              
407             # Next move to the base package directory (we return to the original
408             # directory just before the routine exits)
409              
410             my $pwd = cwd;
411             chdir( "$info->{base_dir}/pkg/$package_name_version" );
412              
413             # ...then ensure that it has all its files
414              
415             my @missing = ExtUtils::Manifest::manicheck;
416             if ( scalar @missing ) {
417             die "Cannot install package $info->{name}-$info->{version} to website ",
418             "-- the base package has files that are specified in MANIFEST missing ",
419             "from the filesystem: @missing. Please fix the situation.\n";
420             }
421              
422             # ...and get all the filenames from MANIFEST
423              
424             my $BASE_FILES = ExtUtils::Manifest::maniread;
425              
426             # Now create the subdirectories and copy the configs
427              
428             $class->create_subdirectories( $pkg_dir, $info->{website_name} );
429             $class->_copy_spops_config_file( $info, $CONFIG, 'spops.perl' );
430             $class->_copy_spops_config_file( $info, $CONFIG, 'spops.perl.ldap' );
431             $class->_copy_action_config_file( $info, $CONFIG );
432              
433             # Now copy over the struct/, script/, data/, template/, html/,
434             # html/images/ and doc/ files -- intact with no translations, as
435             # long as they appear in the MANIFEST file (read in earlier)
436              
437             # The value of the subdir key is the root where the files will be
438             # copied -- so files in the 'widget' directory of the package will
439             # be copied to the 'template/' subdirectory of the website, while
440             # the files in the 'data' directory of the package will be copied
441             # to the 'data' directory of the *package* in the website.
442              
443             my %subdir_match = (
444             struct => "$pkg_dir/struct",
445             data => "$pkg_dir/data",
446             template => "$pkg_dir/template",
447             widget => "$info->{website_dir}/template",
448             doc => "$pkg_dir/doc",
449             script => "$pkg_dir/script",
450             html => "$info->{website_dir}/html" );
451              
452             my $pkg_file_list = [ keys %{ $BASE_FILES } ];
453             foreach my $sub_dir ( sort keys %subdir_match ) {
454             $class->_copy_package_files( $subdir_match{ $sub_dir },
455             $sub_dir,
456             $pkg_file_list );
457             }
458              
459             ########################################
460             # TODO: For each file copied over to the /html directory, create a
461             # 'page' object in the system for it. Note that we might have to
462             # hook this up with the system that ensures we don't overwrite
463             # certain files. So we might need to either remove it from the
464             # _copy_package_files() routine, or add an argument to that
465             # routine that lets us pass in a coderef to execute with every
466             # item copied over.
467              
468             # ACK -- here's a problem. We don't know if we can even create an
469             # $R yet, because (1) the base_page package might not have even
470             # been installed yet (when creating a website) and (2) the user
471             # hasn't yet configured the database (etc.)
472              
473             # We can get around this whenever we rewrite
474             # Package/PackageRepository/oi_manage, but until then we will tell
475             # people to include the relevant data inserts with packages that
476             # include HTML documents.
477              
478             # Until then, here's what this might look like :-)
479              
480             # # Now do the HTML files, but also create records for each of the
481             # # HTML files in the 'page' table
482              
483             # my $copied = $class->_copy_package_files( "$info->{website_dir}/html",
484             # 'html',
485             # $pkg_file_list );
486             # my @html_locations = map { s/^html//; $_ } @{ $copied };
487             # foreach my $location ( @html_locations ) {
488             # my $page = $R->page->fetch( $location, { skip_security => 1 } );
489             # next if ( $page );
490             # eval {
491             # $R->page->new({ location => $location,
492             # ... })
493             # ->save({ skip_security => 1 });
494             # };
495             # }
496              
497             # Now copy the MANIFEST.SKIP file and package.conf, so we can run
498             # 'check_package' on the package directory (once complete) as well as
499             # generate a MANIFEST once we're done copying files
500              
501             foreach my $root_file ( 'MANIFEST.SKIP', 'package.conf' ) {
502             cp( $root_file, "$pkg_dir/$root_file" )
503             || _w( 0, "Cannot copy $root_file to $pkg_dir/$root_file : $!" );
504             }
505              
506             $class->_copy_handler_files( $info, $BASE_FILES );
507              
508             # Now go to our package directory and create a new MANIFEST file
509              
510             chdir( $pkg_dir );
511             $class->_create_manifest();
512              
513             # Finally, save this package information to the site
514              
515             $website_repository->save_package( $info );
516             $website_repository->save();
517              
518             chdir( $pwd );
519             return $pkg_dir;
520             }
521              
522              
523              
524             # Dump the package from the current directory (or the directory
525             # specified in $p->{directory} into a tar.gz distribution file
526              
527             sub export {
528             my ( $class, $p ) = @_;
529             $p ||= {};
530              
531             my $old_pwd = cwd;
532             chdir( $p->{directory} ) if ( -d $p->{directory} );
533              
534             my $cwd = cwd;
535             DEBUG && _w( 1, "Current directory exporting from: [$cwd]" );
536              
537             # If necessary, Read in the config and ensure that it has all the
538             # right information
539              
540             my $config_file = $p->{config_file} || $DEFAULT_CONF_FILE;
541             my $config = $p->{config} ||
542             eval { $class->read_config( { file => $config_file } ) };
543             if ( $@ ) {
544             die "Package configuration file cannot be opened -- \n" ,
545             "are you chdir'd to the package directory? (Reported reason \n",
546             "for failure: $@)\n";
547             }
548             DEBUG && _w( 2, "Package config read in: ", Dumper( $config ) );
549              
550             # Check to ensure that all required fields have something in them; we
551             # might do a 'version' check in the future, but not until it proves
552             # necessary
553              
554             my @missing_fields = ();
555             foreach my $required_field ( @EXPORT_REQUIRED ) {
556             unless ( $config->{ $required_field } ) {
557             push @missing_fields, $required_field;
558             }
559             }
560             if ( scalar @missing_fields ) {
561             die "Configuration file exists [$cwd/$DEFAULT_CONF_FILE] ",
562             "but is missing the following fields: (",
563             join( ', ', @missing_fields ), "). Please add these fields and try again.\n";
564             }
565             DEBUG && _w( 1, "Required fields ok in package configuration file." );
566              
567             # Now, do a check on this package's MANIFEST - are there files in
568             # MANIFEST that don't exist?
569              
570             warn "Package $config->{name}: checking MANIFEST for discrepancies\n";
571             my @missing = ExtUtils::Manifest::manicheck();
572             if ( scalar @missing ) {
573             warn "\nIf the files specified do not need to be in MANIFEST any longer,\n",
574             "please remove them from MANIFEST and re-export the package. Otherwise\n",
575             "users installing the package will get a warning.\n";
576             }
577             else {
578             warn "Looks good\n\n";
579             }
580              
581             # Next see if there are files NOT in the MANIFEST
582              
583             warn "Package $config->{name}: checking filesystem for files not in MANIFEST\n";
584             my @extra = ExtUtils::Manifest::filecheck();
585             if ( scalar @extra ) {
586             warn "\nBuilding a package without these files is OK, but you can also\n",
587             "add them as necessary to the MANIFEST and re-export the package.\n";
588             }
589             else {
590             warn "Looks good\n\n";
591             }
592              
593             # Read in the MANIFEST
594              
595             my $package_files = ExtUtils::Manifest::maniread();
596             DEBUG && _w( 2, "Package info read in:\n", Dumper( $package_files ) );
597              
598             # Now, create a directory of this name-version and copy the files
599              
600             my $package_id = join( '-', $config->{name}, $config->{version} );
601             if ( -d $package_id ) {
602             die "Cannot create directory [$cwd/$package_id] to ",
603             "archive the package because it already exists.\n";
604             }
605             mkdir( $package_id, 0777 )
606             || die "Cannot create directory [$cwd/$package_id] to ",
607             "archive the package! Error: $!";
608             {
609             local $ExtUtils::Manifest::Quiet = 1;
610             ExtUtils::Manifest::manicopy( $package_files, "$cwd/$package_id" );
611             }
612              
613             # And prepend the directory name to all the files so they get
614             # un-archived in the right way
615              
616             my @archive_files = map { "$package_id/$_" } keys %{ $package_files };
617              
618             # Create the tardist
619              
620             my $filename = "$cwd/$package_id.tar.gz";
621             if ( -f $filename ) {
622             $class->_remove_directory_tree( "$cwd/$package_id" );
623             die "Cannot create archive [$filename] - file already exists.\n";
624             }
625             my $rv = eval { $class->_create_archive( $filename, @archive_files ) };
626             die "Error creating archive: $@\n" if ( $@ );
627              
628             # And remove the directory we just created
629              
630             $class->_remove_directory_tree( "$cwd/$package_id" );
631              
632             # Return the filename and the name/version information for the
633             # package distribution we just created
634              
635             chdir( $old_pwd );
636             if ( $rv ) {
637             warn "\n";
638             return { name => $config->{name},
639             version => $config->{version},
640             file => "$filename" };
641             }
642             die "Cannot create distribution [$filename]. Error: ", Archive::Tar->error(), "\n";
643             }
644              
645              
646             #
647             # check_package
648             #
649             # What we check for:
650             # package.conf -- has name, version and author defined; all modules defined exist
651             # conf/*.perl -- pass an 'eval' test (through SPOPS::HashFile)
652             # OpenInteract/*.pm -- pass a 'require' test
653             # MyApp/*.pm -- pass a 'require' test
654             #
655             # Parameters:
656             # package_dir
657             # package_name
658             # website_name (optional)
659              
660             sub check {
661             my ( $class, $p ) = @_;
662             my $status = { ok => 0 };
663             if ( ! $p->{package_dir} and $p->{info} ) {
664             my $main_dir = $p->{info}{website_dir} || $p->{info}{base_dir};
665             $p->{package_dir} = join( '/', $main_dir, $p->{info}{package_dir} );
666             $p->{website_name} = $p->{info}{website_name};
667             }
668             unless ( -d $p->{package_dir} ) {
669             die "No valid package dir to check! (Given: $p->{package_dir})";
670             }
671             my $pwd = cwd;
672             chdir( $p->{package_dir} );
673              
674             # First ensure the package config exists
675              
676             unless ( -f "package.conf" ) {
677             $status->{msg} .= "\n-- Package config (package.conf) does not " .
678             "exist in package!\n";
679             }
680             if ( $p->{website_name} and ! -d "$p->{website_name}/" ) {
681             $status->{msg} .= "\n-- Website directory ($p->{website_name}/) " .
682             "does not exist in package!\n";
683             }
684             return $status if ( $status->{msg} );
685              
686             DEBUG && _w( 1, " - package.conf and website_name directory (if app.) ok" );
687              
688             # Set this after we do the initial sanity checks
689              
690             $status->{ok}++;
691              
692             # This is just a warning
693              
694             if ( -f 'Changes' ) {
695             $status->{msg} .= "\n++ File (Changes) to show package Changelog: ok" ;
696             }
697             else {
698             $status->{msg} .= "\n-- File (Changes) to show package Changelog: DOES NOT EXIST\n" ;
699             }
700              
701             DEBUG && _w( 1, " - Changes file exists" );
702              
703             my $pkg_files = ExtUtils::Manifest::maniread();
704              
705             # Now, first go through the config perl files
706              
707             my @perl_files = grep /^conf.*\.perl$/, keys %{ $pkg_files };
708             foreach my $perl_file ( sort @perl_files ) {
709             DEBUG && _w( 1, " checking perl file ($perl_file)" );
710             my $filestatus = 'ok';
711             my $sig = '++';
712             my $obj = eval { SPOPS::HashFile->new({ filename => $perl_file }) };
713             if ( $@ ) {
714             $status->{ok} = 0;
715             $filestatus = "cannot be read in. $@\n";
716             $sig = '--';
717             }
718             elsif ( $perl_file =~ /spops/ ) {
719             foreach my $spops_key ( keys %{ $obj } ) {
720             my $typeof = ref $obj->{ $spops_key } || 'not a reference';
721             unless ( $typeof eq 'HASH' ) {
722             $status->{ok} = 0;
723             $filestatus = "invalid SPOPS configuration: value of each key must be " .
724             "a hashref and the value [$spops_key] is [$typeof]\n";
725             $sig = '--';
726             }
727             }
728             }
729             $status->{msg} .= "\n$sig File ($perl_file) $filestatus";
730             }
731              
732             # Next all the .pm files - stick the package directory (cwd) into
733             # @INC so we don't have any ambiguity about where the modules
734             # being tested come from
735              
736             unshift @INC, cwd;
737              
738             # We suppress warnings within this block so all the interesting
739             # stuff goes into the status
740              
741             {
742             local $SIG{__WARN__} = sub { return undef };
743             my @pm_files = grep /\.pm$/, keys %{ $pkg_files };
744             foreach my $pm_file ( sort @pm_files ) {
745             DEBUG && _w( 1, " checking module file ($pm_file)" );
746             my $filestatus = 'ok';
747             my $sig = '++';
748             eval { require "$pm_file" };
749             if ( $@ ) {
750             $status->{ok} = 0;
751             $filestatus = "cannot be require'd.\n$@\n";
752             $sig = '--';
753             }
754             $status->{msg} .= "\n$sig File ($pm_file) $filestatus";
755             }
756             }
757              
758             # Check all the .dat files in data/ -- they should be valid perl files.
759              
760             my @data_files = grep /^data\/.*\.dat$/, keys %{ $pkg_files };
761             foreach my $data_file ( sort @data_files ) {
762             DEBUG && _w( 1, " checking data file ($data_file)" );
763             my $filestatus = 'ok';
764             my $sig = '++';
765             eval { $class->read_data_file( $data_file ) };
766             if ( $@ ) {
767             $status->{ok} = 0;
768             $filestatus = "is not a valid Perl structure.\n$@\n";
769             $sig = '--';
770             }
771             $status->{msg} .= "\n$sig File ($data_file) $filestatus";
772             }
773              
774              
775             # See if all the templates pass a basic syntax test -- do not log
776             # 'plugin not found' or 'no providers for template prefix' errors,
777             # since we assume those will be ok when it runs in the environment
778              
779             require Template;
780             my $template = Template->new();
781             my @template_files = grep ! /(\.meta|~|\.bak)$/,
782             grep /^(template|widget)/,
783             keys %{ $pkg_files };
784             my ( $out );
785             my @template_errors_ok = ( 'plugin not found', 'no providers for template prefix', 'file error' );
786             my $template_errors_re = '(' . join( '|', @template_errors_ok ) . ')';
787             foreach my $template_file ( sort @template_files ) {
788             DEBUG && _w( 1, " checking template ($template_file)" );
789             my $filestatus = 'ok';
790             my $sig = '++';
791             eval { $template->process( $template_file, undef, \$out )
792             || die $template->error(), "\n" };
793             if ( $@ ) {
794             unless ( $@ =~ /$template_errors_re/ ) {
795             $status->{ok} = 0;
796             $filestatus = "is not a valid Template Toolkit template.\n$@\n";
797             $sig = '--';
798             }
799             }
800             $status->{msg} .= "\n$sig File ($template_file) $filestatus";
801             }
802              
803             # Now open up the package.conf and check to see that name, version
804             # and author exist
805              
806             DEBUG && _w( 1, " checking package.conf validity" );
807             my $config = $class->read_config({ directory => '.' });
808             $status->{name} = $config->{name};
809             my $conf_msg = '';
810             unless ( $config->{name} ) {
811             $conf_msg .= "\n-- package.conf: required field 'name' is not defined.";
812             }
813             unless ( $config->{version} ) {
814             $conf_msg .= "\n-- package.conf: required field 'version' is not defined.";
815             }
816             unless ( $config->{author} ) {
817             $conf_msg .= "\n-- package.conf: required field 'author' is not defined.";
818             }
819             if ( ref $config->{module} eq 'ARRAY' ) {
820             my @failed_modules = $class->_check_module_install( @{ $config->{module} } );
821             if ( scalar @failed_modules ) {
822             $conf_msg .= "\n-- package.conf: the following modules are used by " .
823             "package but not installed: " .
824             "(" . join( ', ', @failed_modules ) . ") " .
825             "INSTALL THESE PACKAGES BEFORE CONTINUING."
826             }
827             }
828             if ( $conf_msg ) {
829             $status->{msg} .= "$conf_msg\n";
830             $status->{ok} = 0;
831             }
832             else {
833             $status->{msg} .= "\n++ package.conf: ok";
834             }
835              
836             # While we have the package.conf open, see if there are any
837             # modules and whether they're available
838              
839              
840              
841             # Now do the check to ensure that all files in the MANIFEST exist
842             # -- just get feedback from the manifest module, don't let it
843             # print out results of its findings (Quiet)
844              
845             DEBUG && _w( " checking MANIFEST against files" );
846             $ExtUtils::Manifest::Quiet = 1;
847             my @missing = ExtUtils::Manifest::manicheck();
848             if ( scalar @missing ) {
849             $status->{msg} .= "\n-- MANIFEST files not all in package. " .
850             "Following not found: \n " .
851             join( "\n ", @missing ) . "\n";
852             }
853             else {
854             $status->{msg} .= "\n++ MANIFEST files all exist in package: ok";
855             }
856              
857             # Now do the check to see if any extra files exist than are in the MANIFEST
858              
859             my @extra = ExtUtils::Manifest::filecheck();
860             if ( scalar @extra ) {
861             $status->{msg} .= "\n-- Files in package not in MANIFEST:\n " .
862             join( "\n ", @extra ) . "\n";
863             }
864             else {
865             $status->{msg} .= "\n++ All files in package also in MANIFEST: ok";
866             }
867              
868             $status->{msg} .= "\n";
869              
870             chdir( $pwd );
871             return $status;
872             }
873              
874             # Copy all modules from a particular package (site directory AND base
875             # directory) to another directory
876              
877             sub copy_modules {
878             my ( $class, $info, $to_dir ) = @_;
879              
880             my $site_pkg_dir = join( '/', $info->{website_dir}, $info->{package_dir} );
881             my $site_modules = $class->_copy_module_files( $site_pkg_dir, $to_dir );
882              
883             my $base_pkg_dir = join( '/', $info->{base_dir}, $info->{package_dir} );
884             my $base_modules = $class->_copy_module_files( $base_pkg_dir, $to_dir );
885              
886             return [ sort @{ $base_modules }, @{ $site_modules } ];
887             }
888              
889              
890             sub _copy_module_files {
891             my ( $class, $pkg_dir, $to_dir ) = @_;
892             unless ( -d $pkg_dir ) {
893             die "Package directory ($pkg_dir) does not exist -- cannot copy files.\n";
894             }
895             unless ( -d $to_dir ) {
896             die "Destination for package modules ($to_dir) does not exist -- cannot copy files.\n";
897             }
898             my $current_dir = cwd;
899             chdir( $pkg_dir );
900             $to_dir =~ s|/$||;
901             my $pkg_files = ExtUtils::Manifest::maniread;
902             my @module_files = grep /\.pm$/, keys %{ $pkg_files };
903             my @module_files_full = ();
904             my ( %dir_ok );
905             foreach my $filename ( @module_files ) {
906             my $full_dest_file = join( '/', $to_dir, $filename );
907             #warn "Trying to copy file ($filename) to ($full_dest_file)\n";
908             next if ( -f $full_dest_file );
909             my $full_dest_dir = File::Basename::dirname( $full_dest_file );
910             unless ( $dir_ok{ $full_dest_dir } ) {
911             File::Path::mkpath( $full_dest_dir );
912             $dir_ok{ $full_dest_dir }++;
913             }
914             cp( $filename, $full_dest_file );
915             push @module_files_full, $full_dest_file;
916             }
917             chdir( $current_dir );
918             return \@module_files_full;
919             }
920              
921              
922             sub read_data_file {
923             my ( $class, $filename ) = @_;
924             open( D, $filename ) || die "Cannot open: $@";
925             local $/ = undef;
926             my $raw = ;
927             close( D );
928             my ( $dat );
929             {
930             no strict 'vars';
931             $dat = eval $raw;
932             die $@ if ( $@ );
933             }
934             return $dat;
935             }
936              
937             sub remove {
938             my ( $class, $repository, $info, $opt ) = @_;
939             $repository->remove_package( $info );
940             $repository->save();
941             my $base_dir = $info->{website_dir} || $info->{base_dir};
942             my $full_dir = join( '/', $base_dir, $info->{package_dir} );
943             if ( $opt eq 'directory' ) {
944             return $class->_remove_directory_tree( $full_dir );
945             }
946             return 1;
947             }
948              
949              
950             sub read_config {
951             my ( $class, $p ) = @_;
952             if ( ( $p->{info} or $p->{directory} ) and ! $p->{file} ) {
953             my $dir = $p->{directory};
954             unless ( -d $dir ) {
955             $dir = $p->{info}{website_dir} || $p->{info}{base_dir};
956             $dir = join( '/', $dir, $p->{info}{package_dir} );
957             }
958             $p->{file} = join( '/', $dir, $DEFAULT_CONF_FILE );
959             }
960             unless ( -f $p->{file} ) {
961             die "Package configuration file ($p->{file}) does not exist.\n";
962             }
963             open( CONF, $p->{file} ) || die "Error opening $p->{file}: $!";
964             my $config = {};
965             while ( ) {
966             next if ( /^\s*\#/ );
967             next if ( /^\s*$/ );
968             chomp;
969             s/\r//g;
970             s/^\s+//;
971             s/\s+$//;
972             my ( $k, $v ) = split /\s+/, $_, 2;
973             last if ( $k eq 'description' );
974              
975             # If there are multiple values possible, make a list
976              
977             if ( $CONF_LIST_KEYS{ $k } ) {
978             push @{ $config->{ $k } }, $v;
979             }
980              
981             # Otherwise, if it's a key -> key -> value set; add to list
982              
983             elsif ( $CONF_HASH_KEYS{ $k } ) {
984             my ( $sub_key, $sub_value ) = split /\s+/, $v, 2;
985             $config->{ $k }{ $sub_key } = $sub_value;
986             }
987              
988             # If not all that, then simple key -> value
989              
990             else {
991             $config->{ $k } = $v;
992             }
993             }
994              
995             # Once all that is done, read the description in all at once
996             {
997             local $/ = undef;
998             $config->{description} = ;
999             }
1000             chomp $config->{description};
1001             close( CONF );
1002             return $config;
1003             }
1004              
1005              
1006             # Read in a file (parameter 'from_file') and write it to a file
1007             # (parameter 'to_file'), doing replacements on keys along the way. The
1008             # keys are found in the list 'from_text' and the replacements are
1009             # found in the list 'to_text'.
1010              
1011             sub replace_and_copy {
1012             my ( $class, $p ) = @_;
1013             unless ( $p->{from_text} and $p->{to_text}
1014             and $p->{from_file} and $p->{to_file} ) {
1015             die "Not enough params for copy/replace! ", Dumper( $p ), "\n";
1016             }
1017             cp( $p->{from_file}, "$p->{to_file}.old" )
1018             || die "No copy $p->{from_file} -> $p->{to_file}.old: $!";
1019             open( OLD, "$p->{to_file}.old" )
1020             || die "Cannot open copied file: $!";
1021             open( NEW, "> $p->{to_file}" )
1022             || die "Cannot open new file: $!";
1023             while ( ) {
1024             my $line = $_;
1025             for ( my $i = 0; $i < scalar @{ $p->{from_text} }; $i++ ) {
1026             $line =~ s/$p->{from_text}->[ $i ]/$p->{to_text}->[ $i ]/g;
1027             }
1028             print NEW $line;
1029             }
1030             close( NEW );
1031             close( OLD );
1032             unlink( "$p->{to_file}.old" )
1033             || warn qq/Cannot erase temp file (you should do a /,
1034             qq/'rm -f `find . -name "*.old"`' after this is done): $!\n/;
1035             }
1036              
1037              
1038             # Find a file that exists in either the website directory or the base
1039             # installation directory. @file_list defines a number of choices
1040             # available for the file to be named.
1041             #
1042             # Returns: the full path and filename of the first match
1043              
1044             sub find_file {
1045             my ( $class, $info, @file_list ) = @_;
1046             return undef unless ( scalar @file_list );
1047             foreach my $base_file ( @file_list ) {
1048             if ( $info->{website_dir} ) {
1049             my $filename = join( '/', $info->{website_dir}, $info->{package_dir}, $base_file );
1050             DEBUG && _w( 1, "Created filename <<$filename>> using the website directory" );
1051             return $filename if ( -f $filename );
1052             }
1053             my $filename = join( '/', $info->{base_dir}, $info->{package_dir}, $base_file );
1054             DEBUG && _w( 1, "Created filename <<$filename>> using the base installation directory" );
1055             return $filename if ( -f $filename );
1056             }
1057             DEBUG && _w( 1, "No existing filename found matching @file_list" );
1058             return undef;
1059             }
1060              
1061              
1062             # Put the base and website package directories into @INC
1063             #
1064             # NOTE: THIS WILL PROBABLY BE REMOVED
1065              
1066             sub add_to_inc {
1067             my ( $class, $info ) = @_;
1068             my @my_inc = ();
1069             my $base_package_dir = join( '/', $info->{base_dir}, $info->{package_dir} );
1070             unshift @my_inc, $base_package_dir if ( -d $base_package_dir );
1071             if ( $info->{website_dir} ) {
1072             my $app_package_dir = join( '/', $info->{website_dir}, $info->{package_dir} );
1073             unshift @my_inc, $app_package_dir if ( -d $app_package_dir );
1074             }
1075             #unshift @INC, @my_inc;
1076             return @my_inc;
1077             }
1078              
1079              
1080             sub _check_module_install {
1081             my ( $class, @modules ) = @_;
1082             my ( @failed_modules );
1083             MODULE:
1084             foreach my $module ( @modules ) {
1085             next unless ( $module );
1086             if ( $module =~ /\|\|/ ) {
1087             my @alt_modules = split /\s*\|\|\s*/, $module;
1088             foreach my $alt_module ( @alt_modules ) {
1089             eval "require $alt_module";
1090             next MODULE unless ( $@ );
1091             }
1092             push @failed_modules, join( ' or ', @alt_modules );
1093             }
1094             else {
1095             eval "require $module";
1096             push @failed_modules, $module if ( $@ );
1097             }
1098             }
1099             return @failed_modules;
1100             }
1101              
1102              
1103             sub _create_archive {
1104             my ( $class, $filename, @files ) = @_;
1105             return undef unless ( $filename and scalar @files );
1106             DEBUG && _w( 2, "Creating archive ($filename) with files:\n", join( ' -- ', @files ) );
1107             die "file exits" if ( -f $filename );
1108             my $rv = undef;
1109             if ( Archive::Tar->VERSION >= 0.20 ) {
1110             DEBUG && _w( 1, "Creating archive using NEW Archive::Tar syntax." );
1111             $rv = Archive::Tar->create_archive( $filename, 9, @files );
1112             unless ( $rv ) { $ARCHIVE_ERROR = Archive::Tar->error() }
1113             }
1114             else {
1115             DEBUG && _w( 1, "Creating archive using OLD Archive::Tar syntax." );
1116             my $tar = Archive::Tar->new();
1117             $tar->add_files( @files );
1118             $tar->write( $filename, 1 );
1119             if ( $Archive::Tar::error ) {
1120             $ARCHIVE_ERROR = "Possible errors: $Archive::Tar::error / $@ / $!";
1121             }
1122             else {
1123             $rv++;
1124             }
1125             }
1126             return $rv;
1127             }
1128              
1129             # Used to accommodate earlier versions of Archive::Tar (such as those
1130             # shipped with ActivePerl, sigh)
1131              
1132             # * You should already be chdir'd to the directory where this will be
1133             # unpacked
1134              
1135             # * I'm not sure if the version reference below is correct -- I
1136             # *think* it might be 0.20, but I'm not entirely sure.
1137              
1138             sub _extract_archive {
1139             my ( $class, $filename ) = @_;
1140             return undef unless ( -f $filename );
1141             my $rv = undef;
1142             if ( $Archive::Tar::VERSION >= 0.20 ) {
1143             $rv = Archive::Tar->extract_archive( $filename );
1144             unless ( $rv ) { $ARCHIVE_ERROR = Archive::Tar->error() }
1145             }
1146             else {
1147             my $tar = Archive::Tar->new();
1148             $tar->read( $filename, 1 );
1149             my @files = $tar->list_files();
1150             $tar->extract( @files );
1151             if ( $Archive::Tar::error ) {
1152             $ARCHIVE_ERROR = "Possible errors: $Archive::Tar::error / $@ / $!";
1153             }
1154             else {
1155             $rv++;
1156             }
1157             }
1158             return $rv;
1159             }
1160              
1161              
1162             # Copy the spops.perl file from the base install package directory to
1163             # the website package directory Note that we have changed this
1164             # recently (Jan 01) to keep only certain configuration variables
1165             # *behind* -- all others are copied over to the website
1166              
1167             # Also, this works with spops.perl AND spops.perl.IMPL, where 'IMPL'
1168             # right now is generally 'ldap'
1169              
1170             sub _copy_spops_config_file {
1171             my ( $class, $info, $CONFIG, $filename ) = @_;
1172             my $interact_pkg_dir = join( '/', $info->{base_dir}, $info->{package_dir} );
1173             my $website_pkg_dir = join( '/', $info->{website_dir}, $info->{package_dir} );
1174              
1175             my $spops_conf = "conf/$filename";
1176              
1177             unless ( -f "$interact_pkg_dir/$spops_conf" ) {
1178             return undef;
1179             }
1180             my $spops_base = eval { SPOPS::HashFile->new({
1181             filename => "$interact_pkg_dir/$spops_conf" }) };
1182             if ( $@ ) {
1183             _w( 0, "Cannot eval $spops_conf in ($info->{name}-$info->{version}): $@" );
1184             return undef;
1185             }
1186             my $new_config_file = "$website_pkg_dir/$spops_conf";
1187             my $spops_pkg = SPOPS::HashFile->new({
1188             filename => $new_config_file,
1189             perm => 'new' });
1190              
1191             foreach my $spops_key ( keys %{ $spops_base } ) {
1192              
1193             # Change the class to reflect the website name
1194              
1195             if ( my $old_class = $spops_base->{ $spops_key }{class} ) {
1196             $spops_pkg->{ $spops_key }{class} = $class->_change_class_name( $info, $old_class );
1197             }
1198              
1199             # Both the has_a and links_to use class names as keys to link
1200             # objects; change the class names from 'OpenInteract' to the
1201             # website name
1202              
1203             if ( my $old_has_a = $spops_base->{ $spops_key }{has_a} ) {
1204             foreach my $old_class ( keys %{ $old_has_a } ) {
1205             my $new_class = $class->_change_class_name( $info, $old_class );
1206             $spops_pkg->{ $spops_key }{has_a}{ $new_class } = $old_has_a->{ $old_class };
1207             }
1208             }
1209              
1210             if ( my $old_links_to = $spops_base->{ $spops_key }{links_to} ) {
1211             foreach my $old_class ( keys %{ $old_links_to } ) {
1212             my $new_class = $class->_change_class_name( $info, $old_class );
1213             $spops_pkg->{ $spops_key }{links_to}{ $new_class } = $old_links_to->{ $old_class };
1214             }
1215             }
1216              
1217             # Copy over all the fields verbatim except those specified in the
1218             # global %SPOPS_CONF_KEEP. Note that it's ok we're copying
1219             # references here since we're going to dump the information to a
1220             # file anyway
1221              
1222             foreach my $to_copy ( keys %{ $spops_base->{ $spops_key } } ) {
1223             next if ( $SPOPS_CONF_KEEP{ $to_copy } );
1224             next if ( ref $spops_base->{ $spops_key }{ $to_copy } eq 'CODE' );
1225              
1226             # For the 'creation_security', we want to check to see if
1227             # we need to modify the group IDs to match what the server
1228             # has configured
1229              
1230             if ( $to_copy eq 'creation_security' ) {
1231             my ( %new_security );
1232             my $orig = $spops_base->{ $spops_key }{ $to_copy }; # alias to save typing...
1233             foreach my $scope ( keys %{ $orig } ) {
1234             unless ( $scope eq 'g' ) {
1235             $new_security{ $scope } = $orig->{ $scope };
1236             next;
1237             }
1238             next unless ( ref $orig->{g} eq 'HASH' and keys %{ $orig->{g} } );
1239             foreach my $scope_id ( keys %{ $orig->{g} } ) {
1240             my $new_scope = $scope_id;
1241             if ( $scope_id == PUBLIC_GROUP_ID ) {
1242             $new_scope = $CONFIG->{default_objects}{public_group} || PUBLIC_GROUP_ID;
1243             }
1244             elsif ( $scope_id == SITE_ADMIN_GROUP_ID ) {
1245             $new_scope = $CONFIG->{default_objects}{site_admin_group} || SITE_ADMIN_GROUP_ID;
1246             }
1247             $new_security{g}->{ $new_scope } = $orig->{g}{ $scope_id };
1248             }
1249             }
1250             $spops_pkg->{ $spops_key }{ $to_copy } = \%new_security;
1251             }
1252             else {
1253             $spops_pkg->{ $spops_key }{ $to_copy } = $spops_base->{ $spops_key }{ $to_copy };
1254             }
1255             }
1256             }
1257              
1258             eval { $spops_pkg->save({ dumper_level => 1 }) };
1259             die "Cannot save package spops file: $@\n" if ( $@ );
1260             return $new_config_file;
1261             }
1262              
1263              
1264             # Copy the conf/action.perl file over from the base installation to
1265             # the website. This is somewhat easier because there are no nested
1266             # classes we need to modify
1267              
1268             sub _copy_action_config_file {
1269             my ( $class, $info, $CONFIG ) = @_;
1270             my $interact_pkg_dir = join( '/', $info->{base_dir},
1271             $info->{package_dir} );
1272             my $website_pkg_dir = join( '/', $info->{website_dir},
1273             $info->{package_dir} );
1274             DEBUG && _w( 1, "Coping action info from ($interact_pkg_dir)",
1275             "to ($website_pkg_dir)" );
1276              
1277             my $action_conf = 'conf/action.perl';
1278             my $base_config_file = "$interact_pkg_dir/$action_conf";
1279             my $action_base = eval { SPOPS::HashFile->new({
1280             filename => $base_config_file }) };
1281             if ( $@ ) {
1282             DEBUG && _w( 1, "No action info for $info->{name}-$info->{version}",
1283             "(generally ok: $@)" );
1284             return undef;
1285             }
1286              
1287             my $new_config_file = "$website_pkg_dir/$action_conf";
1288             my $action_pkg = eval { SPOPS::HashFile->new({
1289             filename => $new_config_file,
1290             perm => 'new' }) };
1291              
1292             # Go through all of the actions and all of the keys and copy them
1293             # over to the new file. The only modification we make is to a field
1294             # named 'class': if it exists, we modify it to fit in the website's
1295             # namespace.
1296              
1297             foreach my $action_key ( keys %{ $action_base } ) {
1298             foreach my $action_item_key ( keys %{ $action_base->{ $action_key } } ) {
1299             next if ( ref $action_base->{ $action_key }{ $action_item_key } eq 'CODE' );
1300             my $value = $action_base->{ $action_key }{ $action_item_key };
1301             if ( $action_item_key eq 'class' ) {
1302             if ( $value =~ /^OpenInteract::Handler/ ) {
1303             $value = $class->_change_class_name( $info, $value );
1304             }
1305             }
1306             $action_pkg->{ $action_key }{ $action_item_key } = $value;
1307             }
1308             }
1309              
1310             eval { $action_pkg->save({ dumper_level => 1 }) };
1311             die "Cannot save package action file: $@\n" if ( $@ );
1312             return $new_config_file;
1313             }
1314              
1315              
1316             # Copy files from the current (package) directory into a website's
1317             # directory and package
1318              
1319             sub _copy_package_files {
1320             my ( $class, $root_dir, $sub_dir, $file_list ) = @_;
1321             my @copy_file_list = grep /^$sub_dir/, @{ $file_list };
1322             my %no_copy = map { $_ => 1 } $class->read_readonly_file( $root_dir );
1323              
1324             foreach my $sub_dir_file ( @copy_file_list ) {
1325             my $just_filename = $sub_dir_file;
1326             $just_filename =~ s|^$sub_dir/||;
1327             my $new_name = join( '/', $root_dir, $just_filename );
1328             next if ( $no_copy{ $just_filename } );
1329             eval { $class->_create_full_path( $new_name ) };
1330             if ( $@ ) { die "Cannot create path to file ($new_name): $@" }
1331             eval { cp( $sub_dir_file, "$new_name" ) || die $! };
1332             if ( $@ ) {
1333             _w( 0, "Cannot copy ($sub_dir_file) to ($new_name) : $@" );
1334             }
1335             else {
1336             chmod( 0775, $new_name );
1337             }
1338             }
1339             return \@copy_file_list;
1340             }
1341              
1342              
1343             sub read_readonly_file {
1344             my ( $class, $dir ) = @_;
1345             my $overwrite_check_file = join( '/', $dir, READONLY_FILE );
1346             return () unless ( -f $overwrite_check_file );
1347             my ( @no_write );
1348             if ( open( NOWRITE, $overwrite_check_file ) ) {
1349             while ( ) {
1350             chomp;
1351             next if ( /^\s*$/ );
1352             next if ( /^\s*\#/ );
1353             s/^\s+//;
1354             s/\s+$//;
1355             push @no_write, $_;
1356             }
1357             close( NOWRITE );
1358             }
1359             return @no_write;
1360             }
1361              
1362              
1363             # Copy handlers from the base installation to the website directory,
1364             # putting class names into the namespace of the website
1365              
1366             sub _copy_handler_files {
1367             my ( $class, $info, $base_files ) = @_;
1368             my $website_pkg_dir = join( '/', $info->{website_dir},
1369             $info->{package_dir} );
1370              
1371             # We're only operating on the files that begin with
1372             # 'OpenInteract/Handler'...
1373              
1374             my @handler_file_list = grep /^OpenInteract\/Handler/,
1375             keys %{ $base_files };
1376             foreach my $handler_filename ( @handler_file_list ) {
1377              
1378             # First create the old/new class names...
1379              
1380             my $handler_class = $handler_filename;
1381             $handler_class =~ s|/|::|g;
1382             $handler_class =~ s/\.pm$//;
1383             my $new_handler_class = $class->_change_class_name( $info, $handler_class );
1384             DEBUG && _w( 1, "Old name: $handler_class; New name: $new_handler_class" );
1385              
1386             # ... then the new filename
1387              
1388             my $new_filename = "$website_pkg_dir/$handler_filename";
1389             $new_filename =~ s|OpenInteract/Handler|$info->{website_name}/Handler|;
1390              
1391             # Now read in the old handler and write out the new one, replacing
1392             # the 'OpenInteract::Handler::xx' with '$WEBSITE_NAME::Handler::xx'
1393              
1394             open( OLDHANDLER, $handler_filename )
1395             || die "Cannot read handler ($handler_filename): $!";
1396             eval { $class->_create_full_path( $new_filename ) };
1397             if ( $@ ) {
1398             die "Cannot create a dir tree to handler ($new_filename): $@";
1399             }
1400             open( NEWHANDLER, "> $new_filename" )
1401             || die "Cannot write to handler ($new_filename): $!";
1402             while ( ) {
1403             s/$handler_class/$new_handler_class/g;
1404             print NEWHANDLER;
1405             }
1406             close( OLDHANDLER );
1407             close( NEWHANDLER );
1408             }
1409             return \@handler_file_list;
1410             }
1411              
1412              
1413             # auxiliary routine to create necessary directories for a file, given
1414             # the file; die on error, otherwise return a true value
1415              
1416             sub _create_full_path {
1417             my ( $class, $filename ) = @_;
1418             my $dirname = File::Basename::dirname( $filename );
1419             return 1 if ( -d $dirname );
1420             eval { File::Path::mkpath( $dirname, undef, 0755 ) };
1421             return 1 unless ( $@ );
1422             _w( 0, "Cannot create path ($dirname): $@" );
1423             die $@;
1424             }
1425              
1426              
1427             # Create a manifest file in the current directory. (Note that the
1428             # 'Quiet' and 'Verbose' parameters won't work properly until
1429             # ExtUtils::Manifest is patched which won't likely be until 5.6.1)
1430              
1431             sub _create_manifest {
1432             my ( $class ) = @_;
1433             local $SIG{__WARN__} = sub { return undef };
1434             $ExtUtils::Manifest::Quiet = 1;
1435             $ExtUtils::Manifest::Verbose = 0;
1436             ExtUtils::Manifest::mkmanifest();
1437             }
1438              
1439              
1440             # Remove a directory and all files/directories beneath it. Return the
1441             # number of removed files.
1442              
1443             sub _remove_directory_tree {
1444             my ( $class, $dir ) = @_;
1445             my $removed_files = File::Path::rmtree( $dir, undef, undef );
1446             DEBUG && _w( 1, "Removed ($removed_files) files/directories from ($dir)" );
1447             return $removed_files;
1448             }
1449              
1450              
1451             # Modify the first argument by replacing 'OpenInteract' with either
1452             # the second argument or the property 'website_name' of the zeroth
1453             # argument.
1454              
1455             sub _change_class_name {
1456             my ( $class, $info, $old_class, $new_name ) = @_;
1457             if ( ref $info and ! $new_name ) {
1458             $new_name = $info->{website_name};
1459             }
1460             $old_class =~ s/OpenInteract/$new_name/g;
1461             return $old_class;
1462             }
1463              
1464              
1465              
1466             sub _w {
1467             my $lev = shift;
1468             return unless ( DEBUG >= $lev );
1469             my ( $pkg, $file, $line ) = caller;
1470             my @ci = caller(1);
1471             warn "$ci[3] ($line) >> ", join( ' ', @_ ), "\n";
1472             }
1473              
1474             1;
1475              
1476             __END__