Update to current Perl

This commit is contained in:
Joeri de Bruin 2026-02-06 13:40:47 +01:00
parent ebd46d86d4
commit 3cc88f8150
57 changed files with 11638 additions and 665 deletions

3
.gitignore vendored
View file

@ -1 +1,2 @@
/*.kpf
.DS_Store
/*.kpf

169
Dockerfile Normal file
View file

@ -0,0 +1,169 @@
FROM debian:latest
ENV DEBIAN_FRONTEND noninteractive
RUN apt update && apt -y install perl cpanminus libaspell-dev make libdbd-mysql-perl libdigest-perl-md5-perl libxml-simple-perl \
libmodule-install-perl gcc libperl-dev default-libmysqlclient-dev libpng-dev build-essential libgd-dev mariadb-client imagemagick \
libpng-dev libjpeg-dev libtiff-dev libapache2-mod-perl2 libapache2-mod-perl2-dev libapache2-request-perl libimage-magick-perl vim \
apache2 apache2-utils
RUN cpanm --notest --force \
Algorithm::Permute \
App::Cmd \
Archive::Any \
Archive::Tar \
Archive::Zip \
Business::OnlinePayment \
Business::OnlinePayment::AuthorizeNet \
Business::PayPal::API \
Business::Tax::VAT::Validation \
CHI \
CSS::Minifier::XS \
CSS::Packer \
Cache::FastMmap \
Capture::Tiny \
Class::C3 \
Class::InsideOut \
Clone \
Color::Calc \
Compress::Zlib \
Config::JSON \
DBI \
Data::ICal \
DateTime \
DateTime::Event::ICal \
DateTime::Format::HTTP \
DateTime::Format::Mail \
DateTime::Format::Strptime \
DBD::mysql@4.051 \
Devel::StackTrace \
Devel::StackTrace::WithLexicals \
Digest::MD5 \
Digest::SHA \
Email::Valid \
Exception::Class \
Facebook::Graph \
File::Path \
Finance::Quote \
GD \
GD::Graph \
Geo::Coder::Googlev3 \
HTML::Form \
HTML::Highlight \
HTML::Packer \
HTML::Parser \
HTML::TagCloud \
HTML::TagFilter \
HTML::Template \
HTML::Template::Expr \
HTTP::BrowserDetect \
HTTP::Exception \
HTTP::Headers \
HTTP::Request \
IO::File::WithPath \
IO::Interactive::Tiny \
IO::Socket::SSL \
IO::Zlib \
Image::ExifTool \
Imager \
Imager::File::PNG \
JSON \
JSON::Any \
JSON::PP \
JavaScript::Minifier::XS \
JavaScript::Packer \
Kwargs \
LWP \
LWP::Protocol::https \
List::MoreUtils \
Locales \
Log::Log4perl \
MIME::Tools \
Module::Find \
Monkey::Patch \
Moose \
MooseX::NonMoose \
MooseX::Storage \
MooseX::Storage::Format::JSON \
Net::CIDR::Lite \
Net::DNS \
Net::LDAP \
Net::POP3 \
Net::SMTP \
Net::Twitter \
Number::Format \
POE \
POE::Component::Client::HTTP \
POE::Component::IKC::Server \
POE::Component::IKC \
Package::Stash \
Params::Validate \
Path::Class \
PerlIO::eol \
Plack \
Plack::Middleware::Debug \
Plack::Middleware::Status \
Plack::Request \
Plack::Response \
Pod::Coverage \
Readonly \
Scope::Guard \
Search::QueryParser \
Storable \
Template \
Test::Class \
Test::Deep \
Test::Differences \
Test::Exception \
Test::Harness \
Test::Log::Dispatch \
Test::LongString \
Test::MockObject \
Test::MockTime \
Test::More \
Test::Tester \
Test::WWW::Mechanize::PSGI \
Text::Aspell \
Text::Balanced \
Text::CSV_XS \
Tie::CPHash \
Tie::IxHash \
Time::HiRes \
Try::Tiny \
URI::Escape \
UUID::Tiny \
Weather::Com::Finder \
XML::FeedPP \
XML::FeedPP::MediaRSS \
XML::Simple \
common::sense \
namespace::autoclean
#RUN cpanm --notest --force \
# DBD::mysql@4.051
ADD lib /data/WebGUI/lib
ADD sbin /data/WebGUI/sbin
ADD share /data/WebGUI/share
ADD www/extras /data/WebGUI/www/extras
ADD www/maintenance.html /data/WebGUI/www/maintenance.html
ADD www/uploads /data/WebGUI/uploads
ADD etc /data/WebGUI/etc
ADD distribution/webgui/entrypoint /entrypoint
ADD distribution/webgui/www.example.com.conf /etc/apache2/sites-available/000-default.conf
ADD distribution/webgui/webgui.conf /etc/apache2/mods-enabled/webgui.conf
ADD distribution/webgui/modperl.pl /etc/apache2/modperl.pl
RUN useradd --home=/data/WebGUI webgui; chown -R webgui: /data/WebGUI; chmod 755 /entrypoint; \
# apt remove -y cpanminus make gcc libperl-dev ; rm -rf /root/.cpanm; \
echo "ServerName localhost" >> /etc/apache2/apache2.conf;
#USER webgui
WORKDIR /data/WebGUI
CMD [ "/entrypoint" ]

View file

@ -0,0 +1,41 @@
# This file is ideal for development and testing.
# --- DO NOT USE FOR PRODUCTION ---
# however, you can use it as a guide to create a proper production environment.
services:
db:
image: mariadb
container_name: db
environment:
- MYSQL_ROOT_PASSWORD=123qwe
- MYSQL_USER=webgui
- MYSQL_PASSWORD=123qwe
- MYSQL_DATABASE=www_example_com
volumes:
- ./share/create.sql:/docker-entrypoint-initdb.d/1.sql
ports:
- 3306
webgui:
image: webgui
container_name: webgui
depends_on:
- db
volumes:
- ../.:/data/WebGUI
expose:
- "80"
nginx-proxy:
image: nginx:latest
container_name: nginx_proxy
depends_on:
- webgui
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "80:80"
#TODO: add ssl later
#- "443:443"
# TODO: serve staatic files directly from nginx
#- ./www:/var/www/html:ro

View file

@ -0,0 +1,20 @@
worker_processes auto;
events {}
http {
upstream backend {
server webgui:80;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,43 @@
#!/bin/bash
# script that runs when the docker container starts
echo "Starting WebGUI entrypoint script..."
for configfile in `perl -Ilib sbin/wgd for-each`;do
echo "Processing config file: $configfile"
# wait for the db to come up and get our version so we know if we need to run upgrades
connection_info=`perl -Ilib sbin/wgd db --config-file=$configfile --webgui-root=/data/WebGUI --print | sed "s/'//g" `
while true; do
db_version=`mysql --skip-column-names -e 'select webguiVersion from webguiVersion order by dateApplied desc limit 1;' $connection_info ` && break
echo "waiting for the database to come up..."
sleep 10
done
code_version=`perl -Ilib -e 'use WebGUI; print $WebGUI::VERSION;'`
echo "code version $code_version"
echo "database version $db_version for $configfile"
#this table shoud have a defult value. Shoud be in upgrade.pl to new version
`mysql -e 'alter table userSession MODIFY COLUMN userId char(22) NOT NULL DEFAULT "0";' $connection_info ` && break
`mysql -e 'alter table assetVersionTag MODIFY COLUMN name char(255) NOT NULL DEFAULT "default";' $connection_info ` && break
if [ "$db_version" != "$code_version" ];then
perl -Ilib sbin/wgd reset --upgrade --verbose --config-file=$configfile --webgui-root=/data/WebGUI/
### alter table userSession MODIFY COLUMN userId char(22) NOT NULL DEFAULT '0';
## perl -Ilib sbin/wgd db --config-file=www.example.com --webgui-root=/data/WebGUI
fi
done
#perl -I/data/WebGUI/lib sbin/spectre.pl --daemon
service apache2 start
while true; do
echo "Starting Apache web server..."
sleep 60
done
apachectl -DFOREGROUND

View file

@ -0,0 +1,6 @@
#use Apache2::SizeLimit;
#$Apache2::SizeLimit::MAX_PROCESS_SIZE = 600000;
#$Apache2::SizeLimit::MAX_UNSHARED_SIZE = 600000;
#$Apache2::SizeLimit::CHECK_EVERY_N_REQUESTS = 10;
1;

View file

@ -0,0 +1,4 @@
PerlSetVar WebguiRoot /data/WebGUI
#PerlCleanupHandler Apache2::SizeLimit
PerlRequire /data/WebGUI/sbin/preload.perl
PerlRequire /etc/apache2/modperl.pl

View file

@ -0,0 +1,16 @@
<VirtualHost 127.0.0.1:80>
ServerName www.example.com
ServerAlias localhost example.com unitedknowledge.vps2.unitedknowledge.net www.unitedknowledge.eu unitedknowledge.eu www.united-knowledge.eu united-knowledge.eu www.unitedknowledge.net unitedknowledge.net
DocumentRoot /data/WebGUI/www
SetEnvIf SSLPROXY "1" \
SSLPROXY
SetHandler perl-script
PerlInitHandler WebGUI
CustomLog /var/log/apache2/www.example.com.access.log combined
ErrorLog /var/log/apache2/www.example.com.error.log
PerlSetVar WebguiConfig www.example.com.conf
<Directory "/data/WebGUI/www">
Require all granted
</Directory>
</VirtualHost>

File diff suppressed because one or more lines are too long

602
lib/WGDev.pm Normal file
View file

@ -0,0 +1,602 @@
package WGDev;
# ABSTRACT: WebGUI Developer Utilities
use strict;
use warnings;
use 5.008008;
use File::Spec ();
use Cwd ();
use WGDev::X ();
use Try::Tiny;
sub new {
my $class = shift;
my $self = bless {}, $class;
my $root;
my $config;
if ( $_[0] && -d $_[0] ) {
( $root, $config ) = @_;
}
else {
( $config, $root ) = @_;
}
if ($root) {
$self->root($root);
}
if ($config) {
$self->config_file($config);
}
return $self;
}
sub set_environment {
my $self = shift;
my %options = @_;
require Config;
WGDev::X::NoWebGUIRoot->throw
if !$self->root;
WGDev::X::NoWebGUIConfig->throw
if !$self->config_file;
if (! $options{localized}) {
$self->{orig_env}
||= { map { $_ => $ENV{$_} } qw(WEBGUI_ROOT WEBGUI_CONFIG PERL5LIB) };
}
##no critic (RequireLocalizedPunctuationVars)
$ENV{WEBGUI_ROOT} = $self->root;
$ENV{WEBGUI_CONFIG} = $self->config_file;
$ENV{PERL5LIB} = join $Config::Config{path_sep}, $self->lib,
$ENV{PERL5LIB} || ();
return 1;
}
sub reset_environment {
my $self = shift;
my $orig_env = delete $self->{orig_env};
return
if !$orig_env;
##no critic (RequireLocalizedPunctuationVars)
@ENV{ keys %{$orig_env} } = values %{$orig_env};
return 1;
}
sub root {
my $self = shift;
if (@_) {
my $path = shift;
if ( -d $path
&& -e File::Spec->catfile( $path, 'lib', 'WebGUI.pm' )
)
{
$self->{root} = File::Spec->rel2abs($path);
$self->{lib} = File::Spec->catdir( $self->{root}, 'lib' );
unshift @INC, $self->lib;
}
else {
WGDev::X::BadParameter->throw(
'parameter' => 'WebGUI root directory',
'value' => $path,
);
}
}
return $self->{root};
}
sub config_file {
my $self = shift;
if (@_) {
my $path = shift;
require Config::JSON;
if ( -e $path ) {
}
elsif (
$self->root
&& -e (
my $fullpath
= File::Spec->catfile( $self->root, 'etc', $path ) ) )
{
$path = $fullpath;
}
else {
WGDev::X::BadParameter->throw(
'parameter' => 'WebGUI config file',
'value' => $path,
);
}
if ( !$self->root ) {
try {
$self->root(
File::Spec->catpath(
( File::Spec->splitpath($path) )[ 0, 1 ],
File::Spec->updir
) );
};
}
my $path_abs = File::Spec->rel2abs($path);
my $config;
if ( ! try { $config = Config::JSON->new($path_abs) } ) {
WGDev::X::BadParameter->throw(
'parameter' => 'WebGUI config file',
'value' => $path,
);
}
$self->close_session;
$self->close_config;
$self->{config_file} = $path_abs;
$self->{config} = $config;
delete $self->{config_file_relative};
}
return $self->{config_file};
}
sub lib {
my $self = shift;
WGDev::X::NoWebGUIRoot->throw
if !$self->root;
if ( !wantarray ) {
return $self->{lib};
}
my @lib = $self->{lib};
if ( !$self->{custom_lib} ) {
my @custom_lib;
$self->{custom_lib} = \@custom_lib;
my $custom
= File::Spec->catfile( $self->root, 'sbin', 'preload.custom' );
if ( -e $custom && open my $fh, '<', $custom ) {
while ( my $line = <$fh> ) {
$line =~ s/[#].*//msx;
$line =~ s/\A\s+//msx;
$line =~ s/\s+\z//msx;
if ( -d $line ) {
unshift @custom_lib, $line;
}
}
close $fh or WGDev::X::IO::Read->throw( path => $custom );
}
}
unshift @lib, @{ $self->{custom_lib} };
return @lib;
}
sub config {
my $self = shift;
WGDev::X::NoWebGUIConfig->throw
if !$self->config_file;
return $self->{config} ||= do {
require Config::JSON;
Config::JSON->new( $self->config_file );
};
}
sub close_config {
my $self = shift;
delete $self->{config};
# if we're closing the config, we probably want new sessions to pick up
# changes to the file
if ( WebGUI::Config->can('clearCache') ) {
WebGUI::Config->clearCache;
}
return 1;
}
sub config_file_relative {
my $self = shift;
WGDev::X::NoWebGUIConfig->throw
if !$self->config_file;
return $self->{config_file_relative} ||= do {
my $config_dir
= Cwd::realpath( File::Spec->catdir( $self->root, 'etc' ) );
File::Spec->abs2rel( $self->config_file, $config_dir );
};
}
sub db {
my $self = shift;
require WGDev::Database;
return $self->{db} ||= WGDev::Database->new( $self->config );
}
sub session {
my $self = shift;
WGDev::X::NoWebGUIConfig->throw
if !$self->config_file;
require WebGUI::Session;
if ( $self->{session} ) {
my $dbh = $self->{session}->db->dbh;
# if the database handle died, close the session
if ( !$dbh->ping ) {
delete $self->{asset};
( delete $self->{session} )->close;
}
}
return $self->{session} ||= do {
my $session = $self->create_session($self->{session_id});
$self->{session_id} = $session->getId;
$session;
};
}
sub create_session {
my $self = shift;
my $session_id = shift;
my $session;
if ( $self->version->module =~ /^8[.]/msx ) {
$session
= WebGUI::Session->open( $self->config_file,
undef, undef, $session_id );
}
else {
$session
= WebGUI::Session->open( $self->root, $self->config_file_relative,
undef, undef, $session_id );
}
return $session;
}
sub close_session {
my $self = shift;
if ( $self->{session} ) { # if we have a cached session
my $session = $self->session; # get the session, recreating if needed
$session->var->end; # close the session
$session->close;
delete $self->{asset};
delete $self->{session};
}
return 1;
}
sub list_site_configs {
my $self = shift;
my $root = $self->root;
WGDev::X::NoWebGUIRoot->throw
if !$root;
if ( opendir my $dh, File::Spec->catdir( $root, 'etc' ) ) {
my @configs = readdir $dh;
closedir $dh
or WGDev::X::IO::Read->throw('Unable to close directory handle');
@configs = map { File::Spec->catdir( $root, 'etc', $_ ) }
grep { /\Q.conf\E$/msx && !/^(?:spectre|log)\Q.conf\E$/msx }
@configs;
return @configs;
}
return;
}
sub asset {
my $self = shift;
require WGDev::Asset;
return $self->{asset} ||= WGDev::Asset->new( $self->session );
}
sub version {
my $self = shift;
WGDev::X::NoWebGUIRoot->throw
if !$self->root;
require WGDev::Version;
return $self->{version} ||= WGDev::Version->new( $self->root );
}
sub wgd_config { ##no critic (ProhibitExcessComplexity)
my ( $self, $key_list, $value ) = @_;
my $config = \( $self->{wgd_config} );
if ( !${$config} ) {
$config = \( $self->read_wgd_config );
}
my @keys;
if ( ref $key_list && ref $key_list eq 'ARRAY' ) {
@keys = @{$key_list};
}
else {
@keys = split /[.]/msx, $key_list;
}
if ( !${$config} ) {
$config = \( $self->{wgd_config} = {} );
}
while (@keys) {
my $key = shift @keys;
my $numeric = $key ne q{} && $key =~ /^[+]?-?\d*$/msx;
my $type = ref ${$config};
if ( ( !$type && !defined $value )
|| $type eq 'SCALAR'
|| ( $type eq 'ARRAY' && !$numeric ) )
{
return;
}
elsif ( $type eq 'ARRAY' or ( !$type && $numeric ) ) {
if ( !$type ) {
${$config} = [];
}
my ($insert) = $key =~ s/^([+])//msx;
if ( !defined $value
&& ( $insert || !defined ${$config}->[$key] ) )
{
return;
}
if ($insert) {
if ( $key ne q{} ) {
if ( $key < 0 ) {
$key += @{ ${$config} };
}
splice @{ ${$config} }, $key, 0, undef;
}
else {
$key = @{ ${$config} };
}
}
$config = \( ${$config}->[$key] );
}
else {
if ( !$type ) {
${$config} = {};
}
if ( !defined ${$config}->{$key} && !defined $value ) {
return;
}
$config = \( ${$config}->{$key} );
}
if (@keys) {
next;
}
if ($value) {
return ${$config} = $value;
}
return ${$config};
}
return;
}
my $json;
sub read_wgd_config {
my $self = shift;
for my $config_file ( "$ENV{HOME}/.wgdevcfg", '/etc/wgdevcfg' ) {
if ( -e $config_file ) {
my $config;
open my $fh, '<', $config_file or next;
my $content = do { local $/; <$fh> };
close $fh or next;
$self->{wgd_config_path} = Cwd::realpath($config_file);
if ( $content eq q{} ) {
$config = {};
}
else {
if ( !$json ) {
require JSON;
$json = JSON->new;
$json->utf8;
$json->relaxed;
$json->canonical;
$json->pretty;
}
$config = try { $json->decode($content) } || {};
}
return $self->{wgd_config} = $config;
}
}
return $self->{wgd_config} = {};
}
sub write_wgd_config {
my $self = shift;
my $config_path = $self->{wgd_config_path};
if ( !$self->{wgd_config_path} ) {
$config_path = $self->{wgd_config_path} = $ENV{HOME} . '/.wgdevcfg';
}
my $config = $self->{wgd_config} || {};
if ( !$json ) {
require JSON;
$json = JSON->new;
$json->utf8;
$json->relaxed;
$json->canonical;
$json->pretty;
}
my $encoded = $json->encode($config);
$encoded =~ s/\n?\z/\n/msx;
open my $fh, '>', $config_path
or WGDev::X::IO::Write->throw(
message => 'Unable to write config file',
path => $config_path,
);
print {$fh} $encoded;
close $fh
or WGDev::X::IO::Write->throw(
message => 'Unable to write config file',
path => $config_path,
);
return 1;
}
sub my_config {
my $self = shift;
my $key = shift;
my @keys;
if ( ref $key && ref $key eq 'ARRAY' ) {
@keys = @{$key};
}
else {
@keys = split /[.]/msx, $key;
}
my $caller = caller;
my $remove = ( ref $self ) . q{::};
$caller =~ s/^\Q$remove//msx;
unshift @keys, map { lcfirst $_ } split /::/msx, $caller;
return $self->wgd_config( \@keys, @_ );
}
sub yaml_decode {
_load_yaml_lib();
goto &yaml_decode;
}
sub yaml_encode {
_load_yaml_lib();
goto &yaml_encode;
}
sub _load_yaml_lib {
## no critic (ProhibitCascadingIfElse)
no warnings 'redefine';
if ( try { require YAML::XS } ) {
*yaml_encode = \&YAML::XS::Dump;
*yaml_decode = \&YAML::XS::Load;
}
elsif ( try { require YAML::Syck } ) {
*yaml_encode = \&YAML::Syck::Dump;
*yaml_decode = \&YAML::Syck::Load;
}
elsif ( try { require YAML } ) {
*yaml_encode = \&YAML::Dump;
*yaml_decode = \&YAML::Load;
}
elsif ( try { require YAML::Tiny } ) {
*yaml_encode = \&YAML::Tiny::Dump;
*yaml_decode = \&YAML::Tiny::Load;
}
else {
*yaml_encode = *yaml_decode = sub {
WGDev::X->throw('No YAML library available!');
};
}
return;
}
sub DESTROY {
my $self = shift;
local $@;
try {
$self->close_session;
};
return;
}
1;
=head1 SYNOPSIS
use WGDev;
my $wgd = WGDev->new( $webgui_root, $config_file );
my $webgui_session = $wgd->session;
my $webgui_version = $wgd->version->module;
=head1 DESCRIPTION
Performs common actions needed by WebGUI developers, such as recreating their
site from defaults, checking version numbers, exporting packages, and more.
=func C<yaml_encode ( $structure )>
Loads a YAML module if needed and encodes a data structure with it.
=func C<yaml_decode ( $yaml_string )>
Loads a YAML module if needed and decodes a data structure with it.
=method C<new ( [ $root ], [ $config ] )>
Creates a new WGDev object. Optionally accepts a WebGUI root path and config
file. These will be passed on to the C<root> and C<config_file> methods.
=method C<root ( [ $webgui_root ] )>
Sets or returns the WebGUI root path the object will be interacting with. If
the path can't be recognized as a WebGUI root, an error will be thrown. The
return value will always be an absolute path to the WebGUI root.
=method C<config_file ( [ $webgui_config ] )>
Sets or returns the site config file path. The given path can be relative to
the current directory or to the etc directory in the WebGUI root. If the
config file is found and the WebGUI root is not yet set, it will set the root
based on the config file path. If the specified config file can't be found,
an error will be thrown.
=method C<config_file_relative>
Returns the config file path relative to the WebGUI config directory. Useful
for initializing WebGUI sessions, which require the config path to be relative
to that directory.
=method C<lib>
In scalar context, returns the WebGUI library path based on the WebGUI root.
In array context, it also includes the library paths specified in the
F<preload.custom> file.
=method C<list_site_configs>
Returns a list of the available site configuration files in the
C<etc> directory of the specified WebGUI root path. The returned
paths will include the full file path.
=method C<config>
Returns a Config::JSON object based on the file set using C<config_file>.
=method C<session>
Returns a WebGUI session initialized using the WebGUI root and config file.
=method C<asset>
Returns a L<WGDev::Asset> object for simple asset operations.
=method C<db>
Returns a L<WGDev::Database> object for database interaction without starting
a WebGUI session.
=method C<version>
Returns a L<WGDev::Version> object for checking the WebGUI version number in
several different places.
=method C<close_config>
Closes the link to the WebGUI config file. Future calls to C<config> will
load a new object based on the file.
=method C<close_session>
Closes the WebGUI session. If the session object has expired or is no longer
valid, it will first be re-opened, then closed properly.
=method C<set_environment>
Sets the C<WEBGUI_ROOT>, C<WEBGUI_CONFIG>, and C<PERL5LIB> environment variables
based on C<root>, C<config_file>, and C<lib>.
=method C<reset_environment>
Resets the C<WEBGUI_ROOT>, C<WEBGUI_CONFIG>, and C<PERL5LIB> based to what they
were prior to set_environment being called.
=method C<wgd_config ( [ $config_param [, $value ] ] )>
Get or set WGDev config file parameters. Accepts two parameters, the config
directive and optionally the value to set it to. The config directive is the
path in a data structure specified either as an array reference of keys or a
period separated string of keys.
=method C<my_config ( [ $config_param [, $value ] ] )>
Similar to wgd_config, but prefixes the specified path with keys based on the
caller's package. For example, a package of C<WGDev::Command::Reset> becomes
C<command.reset>.
=method C<read_wgd_config>
Reads and parses the WGDev config file into memory. Will be automatically
called by C<wgd_config> as needed.
=method C<write_wgd_config>
Saves the current configuration back to the WGDev config file.
=cut

433
lib/WGDev/Asset.pm Normal file
View file

@ -0,0 +1,433 @@
package WGDev::Asset;
# ABSTRACT: Asset utility functions
use strict;
use warnings;
use 5.008008;
use constant LINE_LENGTH => 78;
use WGDev;
use WGDev::X;
use Try::Tiny;
sub new {
my $class = shift;
my $session = shift;
my $self = bless { session => $session, }, $class;
require WebGUI::Asset;
return $self;
}
sub root {
my $self = shift;
return WebGUI::Asset->getRoot( $self->{session} );
}
sub import_node {
my $self = shift;
return WebGUI::Asset->getImportNode( $self->{session} );
}
sub default_asset { goto &home }
sub home {
my $self = shift;
return WebGUI::Asset->getDefault( $self->{session} );
}
sub by_url {
my $self = shift;
my $asset = WebGUI::Asset->newByUrl( $self->{session}, @_ );
if (! defined $asset) {
WGDev::X::AssetNotFound->throw(asset => $_[0]);
}
return $asset;
}
sub by_id {
my $self = shift;
my ($asset_id, $revision) = @_;
my $asset;
if (WebGUI::Asset->can('newById')) {
$asset = WebGUI::Asset->newById( $self->{session}, $asset_id, $revision );
}
else {
$asset = WebGUI::Asset->new( $self->{session}, $asset_id, undef, $revision );
}
if (! defined $asset) {
WGDev::X::AssetNotFound->throw(asset => $_[0]);
}
return $asset;
}
sub find {
my ( $self, $asset_spec ) = @_;
my $session = $self->{session};
my $asset;
my $e;
if ( $session->id->valid($asset_spec) ) {
try {
$asset = $self->by_id($asset_spec);
}
catch {
$e = $_;
};
}
if ( !$asset ) {
try {
$asset = WebGUI::Asset->newByUrl( $session, $asset_spec );
}
catch {
$e ||= $_;
};
}
if ( $asset && ref $asset && $asset->isa('WebGUI::Asset') ) {
return $asset;
}
if ($e) {
WGDev::X->inflate($e);
}
WGDev::X::AssetNotFound->throw( asset => $asset_spec );
}
my $package_re = qr{
[[:upper:]]\w+
(?: ::[[:upper:]]\w+ )*
}msx;
sub validate_class {
my $self = shift;
my $in_class = my $class = shift;
if (
$class =~ s{\A
# optionally starting with WebGUI::Asset:: or ::
(?:(?:WebGUI::Asset)?::)?
( $package_re )
\z
}{WebGUI::Asset::$1}msx
)
{
my $short_class = $1;
return wantarray ? ( $class, $short_class ) : $class;
}
WGDev::X::BadAssetClass->throw( class => $in_class );
}
sub _gen_serialize_header {
my $self = shift;
my $header_text = shift;
my $header = "==== $header_text ";
$header .= ( q{=} x ( LINE_LENGTH - length $header ) ) . "\n";
return $header;
}
sub serialize {
my ( $self, $asset, $properties ) = @_;
my $class = ref $asset || $asset;
WGDev::X::BadParameter->throw('No asset or class specified')
if not defined $class;
if ( !ref $asset ) {
( my $module = $class . '.pm' ) =~ s{::}{/}msxg;
require $module;
}
my $short_class = $class;
$short_class =~ s/^WebGUI::Asset:://xms;
my ( $asset_properties, $meta, $text )
= $self->_asset_properties( $asset, $properties );
my $basic_yaml = WGDev::yaml_encode( {
'Asset ID' => $asset_properties->{assetId},
'Title' => $asset_properties->{title},
'Menu Title' => $asset_properties->{menuTitle},
'URL' => $asset_properties->{url},
'Parent' => (
ref $asset
? $asset->getParent->get('url')
: $self->import_node->get('url')
),
} );
# filter out unneeded YAML syntax
$basic_yaml =~ s/\A---(?:\Q {}\E)?\s*//msx;
$basic_yaml =~ s/\r?\n/\n/msxg;
$basic_yaml =~ s/[ ]+$//msxg;
# line up colons
$basic_yaml =~ s/^([^:]+):/sprintf("%-12s:", $1)/msxeg;
my $output = $self->_gen_serialize_header($short_class) . $basic_yaml;
for my $field ( sort keys %{$text} ) {
my $value = $text->{$field};
if ( !defined $value ) {
$value = q{~};
}
$value =~ s/\r\n?/\n/msxg;
$output .= $self->_gen_serialize_header($field) . $value . "\n";
}
my $meta_yaml = WGDev::yaml_encode($meta);
$meta_yaml =~ s/\A---(?:\Q {}\E)?\s*//msx;
$meta_yaml =~ s/\r?\n/\n/msxg;
$meta_yaml =~ s/[ ]+$//msxg;
$output .= $self->_gen_serialize_header('Properties') . $meta_yaml . "\n";
return $output;
}
sub _asset_properties {
my $self = shift;
my $class = shift;
my $properties = shift;
my $asset;
if ( ref $class ) {
$asset = $class;
$class = ref $asset;
}
@_ = ($self, $class, $asset, $properties);
if ($class->can('definition')) {
goto &_asset_properties_definition;
}
goto &_asset_properties_meta;
}
sub _asset_properties_definition {
my $self = shift;
my ($class, $asset, $properties) = @_;
my $definition = $class->definition( $self->{session} );
my %text;
my %meta;
my $asset_properties
= { $asset ? %{ $asset->get } : (), $properties ? %{$properties} : (),
};
for my $def ( @{$definition} ) {
while ( my ( $property, $property_def )
= each %{ $def->{properties} } )
{
if ( !defined $asset_properties->{$property}
&& defined $property_def->{defaultValue} )
{
$asset_properties->{$property}
= $self->_get_property_default($property_def);
}
$self->_filter_property(
$property,
$asset_properties->{$property},
ucfirst ( $property_def->{fieldType} || q{} ),
$property_def->{tab},
\%text,
\%meta,
);
}
}
return ( $asset_properties, \%meta, \%text );
}
sub _asset_properties_meta {
my $self = shift;
my ($class, $asset, $properties) = @_;
my %text;
my %meta;
my $asset_properties
= { $asset ? %{ $asset->get } : (), $properties ? %{$properties} : (),
};
for my $property ( $class->meta->get_all_property_list ) {
my $attr = $class->meta->find_attribute_by_name($property);
if ( !defined $asset_properties->{$property} ) {
$asset_properties->{$property} = $attr->default;
}
my $field_type = ucfirst $attr->fieldType;
$self->_filter_property(
$property,
$asset_properties->{$property},
ucfirst $attr->fieldType,
$attr->form->{tab},
\%text,
\%meta,
);
}
return ( $asset_properties, \%meta, \%text );
}
sub _filter_property { ##no critic (ProhibitManyArgs)
my $self = shift;
my ( $property, $value, $field_type, $tab, $text, $meta ) = @_;
if ( $property eq 'title'
|| $property eq 'menuTitle'
|| $property eq 'url' )
{
return;
}
elsif ($field_type eq 'HTMLArea'
|| $field_type eq 'Textarea'
|| $field_type eq 'Codearea' )
{
$text->{$property} = $value;
}
elsif ( $field_type eq 'Hidden' ) {
return;
}
else {
$meta->{ $tab || 'properties' }{$property} = $value;
}
return;
}
my %basic_translation = (
'Title' => 'title',
'Asset ID' => 'assetId',
'Menu Title' => 'menuTitle',
'URL' => 'url',
'Parent' => 'parent',
);
sub deserialize {
my $self = shift;
my $asset_data = shift;
my @text_sections = split m{
^====[ ] # line start, plus equal signs
((?:\w|:)+) # word chars or colons (Perl namespace)
[ ]=+ # space + equals
(?:\n|\z) # end of line or end of string
}msx, $asset_data;
# due to split, there is an extra empty entry at the beginning
shift @text_sections;
my $class = $self->validate_class( shift @text_sections );
my $basic_data = shift @text_sections;
my %sections;
my %properties;
while ( my $section = shift @text_sections ) {
my $section_data = shift @text_sections;
chomp $section_data;
if ( $section_data eq q{~} ) {
$section_data = undef;
}
$sections{$section} = $section_data;
}
if ( my $prop_data = delete $sections{Properties} ) {
my $tabs = WGDev::yaml_decode($prop_data);
%properties = map { %{$_} } values %{$tabs};
}
@properties{ keys %sections } = values %sections;
my $basic_untrans = WGDev::yaml_decode($basic_data);
for my $property ( keys %{$basic_untrans} ) {
if ( $basic_translation{$property} ) {
$properties{ $basic_translation{$property} }
= $basic_untrans->{$property};
}
}
$properties{className} = $class;
return \%properties;
}
sub _get_property_default {
my $self = shift;
my $property_def = shift;
my $default = $property_def->{defaultValue};
my $form_class = $property_def->{fieldType};
if ($form_class) {
$form_class = "WebGUI::Form::\u$form_class";
my $form_module = join q{/}, ( split /::/msx, $form_class . '.pm' );
if ( eval { require $form_module; 1 } ) {
my $form = $form_class->new( $self->{session},
{ defaultValue => $default } );
$default = $form->getDefaultValue;
}
}
return $default;
}
sub export_extension {
my $self = shift;
my $asset = shift;
my $class = ref $asset || $asset;
return
if !defined $class;
my $short_class = $class;
$short_class =~ s/.*:://msx;
my $extension = lc $short_class;
$extension =~ s/(?<!^)[aeiouy]//msxg;
$extension =~ tr/a-z//s;
return $extension;
}
1;
=head1 SYNOPSIS
my $root_node = $wgd->asset->root;
=head1 DESCRIPTION
Performs common actions on assets.
=method C<new ( $session )>
Creates a new object. Requires a single parameter of the WebGUI session to use.
=method C<by_id ( $asset_id )>
Finds an asset based on an asset ID.
=method C<by_url ( $asset_url )>
Finds an asset based on a URL.
=method C<find ( $asset_id_or_url )>
Finds an asset based on either an asset ID or a URL based on the format of
the input.
=method C<home>
An alias for the C<default_asset> method.
=method C<default_asset>
Returns the default WebGUI asset, as will be shown for the URL of C</>.
=method C<root>
Returns the root WebGUI asset.
=method C<import_node>
Returns the Import Node asset.
=method C<serialize ( $asset_or_class )>
Serializes an asset into a string that can be written out to a file.
=method C<deserialize ( $asset_data_text )>
Deserializes a string as generated by C<serialize> into either a hash
reference of properties that can be used to create or update an asset.
=method C<validate_class ( [ $class_name ] )>
Accepts a class name of an asset in either full (C<WebGUI::Asset::Template>) or
short (C<Template>) form. In scalar context, returns the full class name. In
array context, returns an array of the full and the short class name. Will
throw an error if the provided class is not valid.
=method C<export_extension ( $asset_or_class )>
Returns a file extension to use for exporting the given asset or
class. The extension will be the last segment of the class name,
lower cased, with repeated letters and vowels (except for an initial
vowel) removed.
=cut

493
lib/WGDev/Command.pm Normal file
View file

@ -0,0 +1,493 @@
package WGDev::Command;
# ABSTRACT: Run WGDev commands
use strict;
use warnings;
use 5.008008;
use Getopt::Long ();
use File::Spec ();
use Cwd ();
use WGDev::X ();
sub run {
my $class = shift;
local @ARGV = @_;
Getopt::Long::Configure(
qw(default gnu_getopt pass_through no_auto_abbrev));
Getopt::Long::GetOptions(
'h|?|help' => \( my $opt_help ),
'V|ver|version' => \( my $opt_version ),
'F|config-file=s' => \( my $opt_config ),
'R|webgui-root=s' => \( my $opt_root ),
'S|sitename=s' => \( my $opt_sitename ),
) || WGDev::X::CommandLine->throw( usage => $class->usage(0) );
my @params = @ARGV;
my $command_name = shift @params;
my $command_module = eval { $class->get_command_module($command_name) };
if ( $command_name && !$command_module ) {
my $command_exec = $class->_find_cmd_exec($command_name);
if ($command_exec) {
require WGDev::Command::Run;
$command_module = 'WGDev::Command::Run';
unshift @params, $command_exec, $opt_help ? '--help' : (),
$opt_version ? '--version' : ();
undef $opt_help;
undef $opt_version;
}
else {
WGDev::X::CommandLine::BadCommand->throw(
command_name => $command_name,
usage => $class->usage(0),
);
}
}
if ($opt_version) {
$class->report_version( $command_name, $command_module );
}
elsif ($opt_help) {
$class->report_help( $command_name, $command_module );
}
elsif ( !$command_name ) {
print $class->usage(0);
require WGDev::Command::Commands;
return WGDev::Command::Commands->help;
}
else {
require WGDev;
my $wgd = WGDev->new;
$class->guess_webgui_paths(
wgd => $wgd,
root => $opt_root,
config_file => $opt_config,
sitename => $opt_sitename,
);
my $command = $command_module->new($wgd);
return $command->run(@params);
}
return 1;
}
sub get_params_or_defaults {
my $class = shift;
my %params = @_;
my $wgd = $params{wgd};
if ( $params{config_file} && $params{sitename} ) {
WGDev::X::BadParameter->throw(
q{Can't specify both a config file and a sitename});
}
##no tidy
my $webgui_root
= $params{root}
|| $ENV{WEBGUI_ROOT}
|| $wgd->my_config('webgui_root');
##tidy
my $webgui_config;
my $webgui_sitename;
# avoid buggy critic module
##no critic (ProhibitCallsToUndeclaredSubs)
FIND_CONFIG: {
( $webgui_config = $params{config_file} )
&& last FIND_CONFIG;
( $webgui_sitename = $params{sitename} )
&& last FIND_CONFIG;
( $webgui_config = $ENV{WEBGUI_CONFIG} )
&& last FIND_CONFIG;
( $webgui_sitename = $ENV{WEBGUI_SITENAME} )
&& last FIND_CONFIG;
( $webgui_config = $wgd->my_config('webgui_config') )
&& last FIND_CONFIG;
( $webgui_sitename = $wgd->my_config('webgui_sitename') )
&& last FIND_CONFIG;
}
$params{root} = $webgui_root;
$params{config_file} = $webgui_config;
$params{sitename} = $webgui_sitename;
return %params;
}
sub guess_webgui_paths {
my $class = shift;
my %params = $class->get_params_or_defaults(@_);
my $wgd = $params{wgd};
my $webgui_root = $params{root};
my $webgui_config = $params{config_file};
my $webgui_sitename = $params{sitename};
my $e;
# first we need to find the webgui root
if ($webgui_root) {
$wgd->root($webgui_root);
}
# if that didn't set the root and we have a config, try to set it.
# if it is absolute, it will give us a root as well
if ( !$wgd->root && $webgui_config ) {
if ( eval { $class->set_config_by_input( $wgd, $webgui_config ); } ) {
return $wgd
if $wgd->root;
}
else {
$e = WGDev::X->caught || WGDev::X->new($@);
}
}
if ( !$wgd->root ) {
if ( !eval { $class->set_root_relative($wgd); 1 } ) {
# throw error from previous try to set the config
$e->rethrow if $e;
return $wgd;
}
}
if ($webgui_sitename) {
$class->set_config_by_sitename( $wgd, $webgui_sitename );
}
elsif ($webgui_config) {
$class->set_config_by_input( $wgd, $webgui_config );
}
return $wgd;
}
sub set_root_relative {
my ( $class, $wgd ) = @_;
my $dir = Cwd::getcwd();
while (1) {
if ( -e File::Spec->catfile( $dir, 'lib', 'WebGUI.pm' ) ) {
$wgd->root($dir);
last;
}
my $parent
= Cwd::realpath( File::Spec->catdir( $dir, File::Spec->updir ) );
WGDev::X::NoWebGUIRoot->throw
if $dir eq $parent;
$dir = $parent;
}
return $wgd;
}
sub set_config_by_input {
my ( $class, $wgd, $webgui_config ) = @_;
# first, try the specified config file
if ( eval { $wgd->config_file($webgui_config) } ) {
return $wgd;
}
my $e = WGDev::X->caught;
# if that didn't work, try it with .conf appended
if ( $webgui_config !~ /\Q.conf\E$/msx ) {
if ( eval { $wgd->config_file( $webgui_config . '.conf' ) } ) {
return $wgd;
}
}
# if neither normal or alternate config files worked, die
$e->rethrow;
}
sub set_config_by_sitename {
my ( $class, $wgd, $sitename ) = @_;
require Config::JSON;
my @configs = $wgd->list_site_configs;
my $found_config;
my $sitename_regex = qr/ (?:^|[.]) \Q$sitename\E $ /msx;
for my $config_file (@configs) {
my $config = eval { Config::JSON->new($config_file) };
next
if !$config;
for my $config_sitename ( @{ $config->get('sitename') } ) {
if ( $config_sitename =~ m/$sitename_regex/msx ) {
if ($found_config) {
WGDev::X->throw("Ambigious site name: $sitename");
}
$found_config = $config_file;
}
}
}
if ($found_config) {
$wgd->config_file($found_config);
return $wgd;
}
WGDev::X->throw("Unable to find config file for site: $sitename");
}
sub report_version {
my ( $class, $name, $module ) = @_;
if ( ref $class ) {
$class = ref $class;
}
print "$class version " . $class->VERSION;
if ($module) {
print " - $module version " . $module->VERSION;
}
print "\n";
return 1;
}
sub report_help {
my ( $class, $name, $module ) = @_;
if ( ref $class ) {
$class = ref $class;
}
if ($module) {
if ( $module->can('usage') ) {
print $module->usage(1);
}
else {
warn "No documentation for $name command.\n";
}
}
else {
print $class->usage(1);
}
return 1;
}
sub get_command_module {
my ( $class, $command_name ) = @_;
if ( $command_name && $command_name =~ /^\w+(?:-\w+)*$/mxs ) {
my $module = $class->command_to_module($command_name);
( my $module_file = "$module.pm" ) =~ s{::}{/}mxsg;
if ( eval { require $module_file; 1 }
&& $module->can('run')
&& $module->can('is_runnable')
&& $module->is_runnable )
{
return $module;
}
}
WGDev::X::BadCommand->throw( 'command_name' => $command_name );
}
sub command_to_module {
my ( $class, $command ) = @_;
my $module = join q{::}, __PACKAGE__, map {ucfirst} split /-/msx,
$command;
return $module;
}
sub _find_cmd_exec {
my ( $class, $command_name, $root, $config ) = @_;
if ($command_name) {
for my $path ( File::Spec->path ) {
my $execpath = File::Spec->catfile( $path, "wgd-$command_name" );
if ( -x $execpath ) {
return $execpath;
}
}
}
return;
}
sub usage {
my $class = shift;
require WGDev::Help;
return WGDev::Help::package_usage( $class, @_ );
}
sub command_list {
my $class = shift;
my %commands;
( my $fn_prefix = $class ) =~ s{::}{/}msxg;
require File::Find;
my %lib_check;
for my $inc_path (@INC) {
##no critic (ProhibitParensWithBuiltins)
my $command_root
= File::Spec->catdir( $inc_path, split( /::/msx, $class ) );
next
if !-d $command_root;
my $find_callback = sub {
return
if !/\Q.pm\E$/msx;
no warnings 'once';
my $lib_path
= File::Spec->abs2rel( $File::Find::name, $inc_path );
$lib_check{$lib_path} = 1;
};
File::Find::find( { no_chdir => 1, wanted => $find_callback },
$command_root );
}
no warnings 'once';
for my $module ( grep {m{^\Q$fn_prefix\E/}msx} ( keys %INC, @App::WGDev::PACKED ) ) {
$lib_check{$module} = 1;
}
for my $module ( keys %lib_check ) {
my $package = $module;
$package =~ s/\Q.pm\E$//msx;
$package = join q{::}, File::Spec->splitdir($package);
##no critic (RequireCheckingReturnValueOfEval)
eval {
require $module;
if ( $package->can('run')
&& $package->can('is_runnable')
&& $package->is_runnable
) {
( my $command = $package ) =~ s/^\Q$class\E:://msx;
$command = join q{-}, map {lcfirst} split m{::}msx, $command;
$commands{$command} = 1;
}
};
}
for my $command ( map { glob File::Spec->catfile( $_, 'wgd-*' ) }
File::Spec->path )
{
next
if !-x $command;
my $file = ( File::Spec->splitpath($command) )[2];
$file =~ s/^wgd-//msx;
$commands{$file} = 1;
}
my @commands = sort keys %commands;
return @commands;
}
1;
=head1 SYNOPSIS
wgd [arguments] <subcommand> [subcommand arguments]
=head1 DESCRIPTION
Runs sub-commands from the C<WGDev::Command> namespace, or standalone
scripts starting with F<wgd->
=head1 OPTIONS
=over 8
=item C<-h> C<-?> C<--help>
Display usage summary for any command.
=item C<-V> C<--version>
Display version information
=item C<-F> C<--config-file>
Specify WebGUI config file to use. Can be absolute, relative to
the current directory, or relative to WebGUI's config directory.
If not specified, it will try to use the C<WEBGUI_CONFIG> environment
variable or the C<command.webgui_config> option from the configuration
file.
=item C<-S> C<--sitename>
Specify the name of a WebGUI site to operate on. This will check
all of the config files in WebGUI's config directory for a single
site using the specified C<sitename>. If not specified, the
C<WEBGUI_SITENAME> environment variable and C<command.webgui_sitename>
option will be used if available.
=item C<-R> C<--webgui-root>
Specify WebGUI's root directory. Can be absolute or relative. If
not specified, first the C<WEBGUI_ROOT> environment variable and
C<command.webgui_root> option from the configuration file will be
checked, then will search upward from the current path for a WebGUI
installation.
=item C<< <subcommand> >>
The sub-command to run or get help for.
=back
=method C<run ( @arguments )>
Runs C<wgd>, processing the arguments specified and running a sub-command if possible.
=method C<usage ( [$verbosity] )>
Returns usage information for C<wgd>. The verbosity level is passed on
to L<WGDev::Help::package_usage|WGDev::Help/package_usage>.
=method C<command_list>
Searches for available sub-commands and returns them as an array.
This list includes available Perl modules that pass the
L</get_command_module> check and executable files beginning with
F<wgd->.
=method C<command_to_module ( $command )>
Converts a command into the module that would implement it. Returns
that module name.
=method C<get_command_module ( $command )>
Converts the command to a module, then attempts to load that module.
If the module loads successfully, implements the C<run> and
C<is_runnable> methods, and C<is_runnable> returns true, returns
the module. If not, returns C<undef>.
=method C<< get_params_or_defaults ( wgd => $wgd, %params ) >>
Finds the specified WebGUI root, config file, and C<sitename>. Uses
environment variables and configuration file if not specified
directly. Returns C<%params> with C<root>, C<config_file>, and
C<sitename> options updated.
=method C<< guess_webgui_paths ( wgd => $wgd, [root => $webgui_root], [config_file => $webgui_config] ) >>
Attempts to detect the paths to use for the WebGUI root and config
file. Initializes the specified C<$wgd> object. If specified, attempts
to use the specified paths first. If not specified, first checks
the environment variables C<WEBGUI_ROOT> and C<WEBGUI_CONFIG>.
Next, attempts to search upward from the current path to find the
WebGUI root. If a WebGUI root has been found but not a config file,
checks for available config files. If only one is available, it
is used as the config file.
=method C<set_root_relative ( $wgd )>
Attempts to set the root WebGUI directory based on the current
directory. Searches upward from the current path for a valid WebGUI
root directory, and sets it in the C<$wgd> object if found. If no
valid root is found, throws an error.
=method C<set_config_by_input ( $wgd, $config )>
Sets the config file in the C<$wgd> object based on the specified
WebGUI config file. If the specified file isn't found, but a file
with the same name with the C<.conf> extension added to it does
exist, that file will be used. If a config file can't be found,
throws an error.
=method C<set_config_by_sitename ( $wgd, $sitename )>
Sets the config file in the C<$wgd> object based on the specified
site name. All of the available config files will be checked and
if one of the sites lists the site name, its config file will be
used.
=method C<report_help ( [$command, $module] )>
Shows help information for C<wgd> or a sub-command. If a command
and module is specified, attempts to call C<usage> on the module
or displays an error. Otherwise, displays help information for
C<wgd>.
=method C<report_version ( [$command, $module] )>
Reports version information about C<wgd>. If specified, also
includes version information about a sub-command.
=cut

270
lib/WGDev/Command/Base.pm Normal file
View file

@ -0,0 +1,270 @@
package WGDev::Command::Base;
# ABSTRACT: Super-class for implementing WGDev commands
use strict;
use warnings;
use 5.008008;
use WGDev::X ();
sub is_runnable {
my $class = shift;
return $class->can('process');
}
sub new {
my ( $class, $wgd ) = @_;
my $self = bless {
wgd => $wgd,
options => {},
arguments => [],
}, $class;
return $self;
}
sub wgd { return $_[0]->{wgd} }
sub parse_params {
my $self = shift;
local @ARGV = @_;
require Getopt::Long;
Getopt::Long::Configure( 'default', $self->config_parse_options );
my %getopt_params = (
'<>' => sub {
$self->argument( map {"$_"} @_ );
},
);
for my $option ( $self->config_options ) {
# for complex options, name is first word segment
( my $option_name ) = ( $option =~ /([\w-]+)/msx );
my $method = 'option_' . $option_name;
$method =~ tr/-/_/;
if ( $self->can($method) ) {
$getopt_params{$option} = sub {
$self->$method( @_[ 1 .. $#_ ] );
};
}
else {
$getopt_params{$option} = \( $self->{options}{$option_name} );
}
}
my $result = Getopt::Long::GetOptions(%getopt_params);
push @{ $self->{arguments} }, @ARGV;
return $result;
}
sub parse_params_string {
my $self = shift;
my $param_string = shift;
require Text::ParseWords;
return $self->parse_params( Text::ParseWords::shellwords($param_string) );
}
sub config_parse_options { return qw(gnu_getopt) }
sub config_options { }
sub option {
my $self = shift;
my $option = shift || return;
if (@_) {
return $self->{options}{$option} = shift;
}
return $self->{options}{$option};
}
sub set_option_default {
my $self = shift;
my $option = shift || return;
if ( !defined $self->option($option) ) {
return $self->option( $option, @_ );
}
return;
}
sub argument {
my $self = shift;
if (@_) {
push @{ $self->{arguments} }, @_;
return wantarray ? @_ : $_[-1];
}
return;
}
sub arguments {
my $self = shift;
if ( @_ && ref $_[0] eq 'ARRAY' ) {
my $arguments = shift;
@{ $self->{arguments} } = @{$arguments};
}
return @{ $self->{arguments} };
}
sub run {
my $self = shift;
WGDev::X::NoWebGUIRoot->throw
if $self->needs_root && !$self->wgd->root;
WGDev::X::NoWebGUIConfig->throw
if $self->needs_config && !$self->wgd->config_file;
my @params = ( @_ == 1 && ref $_[0] eq 'ARRAY' ) ? @{ +shift } : @_;
local $| = 1;
if ( !$self->parse_params(@params) ) {
my $usage = $self->usage(0);
WGDev::X::CommandLine::BadParams->throw( usage => $usage );
}
return $self->process;
}
sub usage {
my $class = shift;
my $verbosity = shift;
if ( ref $class ) {
$class = ref $class;
}
require WGDev::Help;
my $usage = WGDev::Help::package_usage( $class, $verbosity );
return $usage;
}
sub help {
my $class = shift;
if ( ref $class ) {
$class = ref $class;
}
require WGDev::Help;
WGDev::Help::package_perldoc( $class,
'!AUTHOR|LICENSE|METHODS|SUBROUTINES' );
return 1;
}
sub needs_root {
return 1;
}
sub needs_config {
my $class = shift;
return $class->needs_root;
}
1;
=head1 SYNOPSIS
package WGDev::Command::Mine;
use parent qw(WGDev::Command::Base);
sub process {
my $self = shift;
print "Running my command\n";
return 1;
}
=head1 DESCRIPTION
A super-class useful for implementing L<WGDev> command modules. Includes
simple methods to override for parameter parsing and provides help text via
Pod::Usage.
While using WGDev::Command::Base is not required to write a command module,
it is the recommended way to do so.
=method C<is_runnable>
This is a class method that must be implemented and return true for all
command modules. This method will return true for any subclass that
implements the C<process> method.
=method C<new ( $wgd )>
Instantiate a new command object. Requires a L<WGDev> object as the first
parameter.
=method C<wgd>
Returns the L<WGDev> object used to instantiate the object.
=method C<config_parse_options>
Returns an array of parameters used to configure command line parsing. These
options are passed directly to L<Getopt::Long>. See
L<Getopt::Long/Configuring_Getopt::Long> for details on the available options.
By default, returns C<gnu_getopt> and can be overridden to return others.
=method C<config_options>
Returns an array of command line options to be parsed. Should be overridden
to set which options will be parsed. Should be specified in the syntax
accepted by L<Getopt::Long>. Each option will be saved as the the first
group of word characters in the option definition. Alternately, if a method
with the name C<< option_<name> >> exists, it will be called to set the
option instead.
=method C<option ( $option [, $value] )>
Sets or returns a command line option. Accepts the option name as the first
parameter. If specified, the option will be set the the value of the second
parameter.
=method C<argument ( $argument )>
Adds an argument to the argument list. Any parameters specified will be added
to the argument list. Can be overridden to provide alternate behavior.
=method C<arguments ( [ \@arguments ] )>
Sets or returns the arguments list. If specified, the first parameter
must be an array reference whose values will be set as the arguments list.
=method C<parse_params ( @parameters )>
Sets options based on an array of command line parameters.
=method C<parse_params_string ( $parameters )>
Sets options based on a string of command line parameters. The string will be
processed with L<Text::ParseWords> C<shellwords> sub then passed on to
C<parse_params>.
=method C<set_option_default ( $option, $value )>
Sets an option only if it is not currently defined. First parameter is the
option to set, second parameter is the value to set it to.
=method C<needs_root>
Should be overridden in subclasses to set whether a command needs a WebGUI root directory to run. Returns true if not overridden.
=method C<needs_config>
Should be overridden in subclasses to set whether a command needs a WebGUI config file directory to run. Returns the same value as L</needs_root> if not overridden.
=method C<usage ( [ $verbosity ] )>
Returns the usage information for the command. The optional first parameter
is the verbosity to use.
=method C<help>
Display help information for this command using L<perldoc>. Excludes AUTHOR
and LICENSE sections.
=method C<run ( @arguments )>
Runs the command. Parameters should be the command line parameters
to use for running the command. This sub should return a true value
on success and either die or return a false value on failure. The
default method will first call C<process_params> with the given
parameters, call C<usage> if there was a problem with parsing the
parameters, or call C<process> if there was not. It will return
C<process>'s return value to the caller.
=method C<process>
Needs to be subclasses to provide the main functionality of the command. This
method will be called as part of the run method. Should return a true value
on success.
=cut

View file

@ -0,0 +1,97 @@
package WGDev::Command::Base::Verbosity;
# ABSTRACT: Super-class for implementing WGDev commands with verbosity levels
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->{verbosity} = 1;
$self->{tab_level} = 0;
return $self;
}
sub config_options {
return qw(
verbose|v
quiet|q
);
}
sub option_verbose {
my $self = shift;
$self->{verbosity}++;
return;
}
sub option_quiet {
my $self = shift;
$self->{verbosity}--;
return;
}
sub verbosity {
my $self = shift;
if (@_) {
return $self->{verbosity} = shift;
}
return $self->{verbosity};
}
sub report {
my $self = shift;
my $message = pop;
my $verbose_limit = shift;
if ( !defined $verbose_limit ) {
$verbose_limit = 1;
}
return
if $verbose_limit > $self->verbosity;
my $tabs = "\t" x $self->tab_level;
print $tabs . $message;
return 1;
}
sub tab_level {
my $self = shift;
if (@_) {
$self->{tab_level} += shift;
}
return $self->{tab_level};
}
1;
=head1 SYNOPSIS
package WGDev::Command::Mine;
use WGDev::Command::Base::Verbosity;
@ISA = qw(WGDev::Command::Base::Verbosity);
sub process {
my $self = shift;
$self->report("Running my command\n");
return 1;
}
=head1 DESCRIPTION
A super-class useful for implementing WGDev command modules. Parses the
C<--verbose> and C<--quiet> command line options.
=method C<verbosity ( [ $verbosity ] )>
Sets or returns the verbosity. This is modified when parsing parameters. Defaults to 1.
=method C<report ( [ $verbosity, ] $message )>
Prints messages based on the current verbosity level. If given two
parameters, the first must be the verbosity level to start printing the
message at. The second parameter is the message to print. Will also accept
a single parameter of a message to print starting at verbosity level 1.
=cut

View file

@ -0,0 +1,218 @@
package WGDev::Command::Batchedit;
# ABSTRACT: Edits assets by URL or asset ID with a pattern and a string
# so it can be used in a shell script / batch file
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev ();
sub config_options {
return qw(
command=s
tree=s@
class=s@
pattern=s
string=s
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my @assets_to_edit = $self->get_assets_data;
if ( !@assets_to_edit ) {
WGDev::X->throw('No assets to edit!');
}
# get pattern to match
my $pattern = $self->{options}->{pattern};
# get replacement string
my $string = $self->{options}->{string};
my $output_format = "%-8s: %-30s (%22s) %s\n";
my $version_tag;
for my $asset_to_edit (@assets_to_edit) {
my $asset_text = $asset_to_edit->{text};
my $old_asset_text = $asset_text;
$asset_text =~ s/$pattern/$string/xmsg;
if ( $asset_text eq $old_asset_text ) {
printf $output_format,
'Skipping', ( $asset_to_edit->{url} || $asset_to_edit->{title} ),
( $asset_to_edit->{asset_id} || q{} ), $asset_to_edit->{title};
next;
}
$version_tag ||= do {
require WebGUI::VersionTag;
my $vt = WebGUI::VersionTag->getWorking( $wgd->session );
$vt->set( { name => 'WGDev Asset Editor' } );
$vt;
};
my $asset_data = $wgd->asset->deserialize($asset_text);
my $asset;
my $parent;
if ( $asset_data->{parent} ) {
$parent = eval { $wgd->asset->find( $asset_data->{parent} ) };
}
if ( $asset_to_edit->{asset_id} ) {
$asset = $wgd->asset->by_id( $asset_to_edit->{asset_id}, undef,
$asset_to_edit->{revision} );
$asset = $asset->addRevision(
$asset_data,
undef,
{
skipAutoCommitWorkflows => 1,
skipNotification => 1,
} );
if ($parent) {
$asset->setParent($parent);
}
}
else {
$parent ||= $wgd->asset->import_node;
my $asset_id = $asset_data->{assetId};
$asset = $parent->addChild(
$asset_data,
$asset_id,
undef,
{
skipAutoCommitWorkflows => 1,
skipNotification => 1,
} );
}
printf $output_format, ( $asset_to_edit->{asset_id} ? 'Updating' : 'Adding' ),
$asset->get('url'), $asset->getId, $asset->get('title');
}
if ($version_tag) {
$version_tag->commit;
}
return 1;
}
sub get_assets_data {
my $self = shift;
my $wgd = $self->wgd;
my @assets_data;
for my $asset_spec ( $self->arguments ) {
my $asset_data = eval { $self->get_asset_data($asset_spec) };
if ( !$asset_data ) {
warn $@;
next;
}
push @assets_data, $asset_data;
}
if ( !$self->option('tree') ) {
return @assets_data;
}
for my $parent_spec ( @{ $self->option('tree') } ) {
my $parent = $wgd->asset->find($parent_spec) || do {
warn "$parent_spec is not a valid asset!\n";
next;
};
my $options = {};
if ( $self->option('class') ) {
my @classes = @{ $self->option('class') };
for (@classes) {
s/^(?:(?:WebGUI::Asset)?::)?/WebGUI::Asset::/msx;
}
$options->{includeOnlyClasses} = \@classes;
}
my $assets
= $parent->getLineage( [qw(self descendants)], $options );
for my $asset_id ( @{$assets} ) {
my $asset_data = $self->get_asset_data($asset_id);
if ( !$asset_data ) {
next;
}
push @assets_data, $asset_data;
}
}
return @assets_data;
}
sub get_asset_data {
my $self = shift;
my $asset = shift;
my $wgd_asset = $self->wgd->asset;
if ( !ref $asset ) {
$asset = eval { $wgd_asset->find($asset) };
if ( !$asset ) {
die $@;
}
}
my $asset_text = $self->wgd->asset->serialize($asset);
my $short_class = ref $asset || $asset;
$short_class =~ s/^WebGUI::Asset:://msx;
return {
text => $asset_text,
class => ref $asset || $asset,
asset_id => $asset->getId,
url => $asset->get('url'),
title => $asset->get('title'),
};
}
1;
=head1 SYNOPSIS
wgd batchedit --pattern=<pattern> --string=<string> <asset> [<asset> ...]
wgd batchedit --tree=<asset> --pattern=<pattern> --string=<string> [--tree=<asset> ...] [--class=<class> ...]
=head1 DESCRIPTION
Edits assets in-place by replacing all matching 'pattern's with 'string'.
If modifications are made, the assets are updated.
=head1 OPTIONS
=over 8
=item C<--pattern=>
Pattern to match against for replacing.
=item C<--string=>
Replacement string for the matched pattern.
=item C<< <asset> >>
Either an asset URL or ID. As many as desired can be specified.
Prepending with a slash will force it to be interpreted as a URL.
=item C<--tree=>
Will open specified asset and all descendants in editor. Can be specified
multiple times.
=item C<--class=>
Only used with --tree option. Limits exported assets to specified classes.
Can be specified as a full (C<WebGUI::Asset::Template>) or abbreviated
(C<Template>) class name.
=back
=method C<get_assets_data>
Creates and returns an array of hash references with information about
the assets and exported files. Also follows the C<--tree> option.
=method C<get_asset_data ( $asset_or_class )>
Accepts an asset, returning a hash reference of information about the
asset.
=cut

243
lib/WGDev/Command/Build.pm Normal file
View file

@ -0,0 +1,243 @@
package WGDev::Command::Build;
# ABSTRACT: Builds an SQL script and uploads for site creation
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base::Verbosity);
use File::Spec ();
use WGDev::X ();
use WGDev::File;
sub config_options {
return (
shift->SUPER::config_options, qw(
sql|s
uploads|u
) );
}
sub parse_params {
my $self = shift;
my $result = $self->SUPER::parse_params(@_);
if ( !defined $self->option('sql') && !defined $self->option('uploads') )
{
$self->option( 'sql', 1 );
$self->option( 'uploads', 1 );
}
return $result;
}
sub process {
my $self = shift;
if ( $self->option('sql') ) {
$self->create_db_script;
}
if ( $self->option('uploads') ) {
$self->update_local_uploads;
}
return 1;
}
sub create_db_script {
my $self = shift;
my $wgd = $self->wgd;
my $version = $wgd->version->database( $wgd->db->connect );
$self->report("WebGUI version: $version\n");
$self->report('Creating database dump... ');
my $wg8 = $wgd->version->module =~ /^8[.]/msx;
my $db_file = $wg8 ? do {
require WebGUI::Paths;
WebGUI::Paths->defaultCreateSQL;
} : File::Spec->catfile( $wgd->root, 'docs', 'create.sql' );
open my $out, q{>}, $db_file
or WGDev::X::IO::Write->throw( path => $db_file );
$self->write_db_header($out);
$self->write_db_structure($out);
$self->write_db_data($out);
$self->write_db_footer($out);
close $out
or WGDev::X::IO::Write->throw( path => $db_file );
$self->report("Done.\n");
return 1;
}
sub write_db_header {
my $self = shift;
my $out = shift;
print {$out} <<'END_SQL';
SET @OLD_CHARACTER_SET_CLIENT = @@CHARACTER_SET_CLIENT;
SET @OLD_CHARACTER_SET_RESULTS = @@CHARACTER_SET_RESULTS;
SET @OLD_CHARACTER_SET_CONNECTION = @@CHARACTER_SET_CONNECTION;
SET @OLD_COLLATION_CONNECTION = @@COLLATION_CONNECTION;
SET @OLD_TIME_ZONE = @@TIME_ZONE;
SET @OLD_UNIQUE_CHECKS = @@UNIQUE_CHECKS;
SET @OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS;
SET @OLD_SQL_MODE = @@SQL_MODE;
SET @OLD_SQL_NOTES = @@SQL_NOTES;
SET CHARACTER_SET_CLIENT = 'utf8';
SET CHARACTER_SET_RESULTS = 'utf8';
SET CHARACTER_SET_CONNECTION = 'utf8';
SET TIME_ZONE = '+00:00';
SET UNIQUE_CHECKS = 0;
SET FOREIGN_KEY_CHECKS = 0;
SET SQL_MODE = 'NO_AUTO_VALUE_ON_ZERO';
SET SQL_NOTES = 0;
END_SQL
return;
}
sub write_db_footer {
my $self = shift;
my $out = shift;
print {$out} <<'END_SQL';
SET CHARACTER_SET_CLIENT = @OLD_CHARACTER_SET_CLIENT;
SET CHARACTER_SET_RESULTS = @OLD_CHARACTER_SET_RESULTS;
SET CHARACTER_SET_CONNECTION = @OLD_CHARACTER_SET_CONNECTION;
SET COLLATION_CONNECTION = @OLD_COLLATION_CONNECTION;
SET TIME_ZONE = @OLD_TIME_ZONE;
SET UNIQUE_CHECKS = @OLD_UNIQUE_CHECKS;
SET FOREIGN_KEY_CHECKS = @OLD_FOREIGN_KEY_CHECKS;
SET SQL_MODE = @OLD_SQL_MODE;
SET SQL_NOTES = @OLD_SQL_NOTES;
END_SQL
return;
}
sub write_db_structure {
my $self = shift;
my $out = shift;
my $wgd = $self->wgd;
open my $in, q{-|}, 'mysqldump',
$wgd->db->command_line( '--compact', '--no-data',
'--compatible=mysql40' )
or WGDev::X::System->throw('Unable to run mysqldump');
my $statement;
while ( my $line = <$in> ) {
next
if $line =~ /\bSET[^=]+=\s*[@][@]character_set_client;/msxi
|| $line =~ /\bSET\s+character_set_client\b/msxi;
if ( !$statement && $line =~ /\A(CREATE[ ]TABLE)/msx ) {
$statement = $1;
}
if ( $statement && $line =~ /;\n?\z/msx ) {
if ( $statement eq 'CREATE TABLE' ) {
$line =~ s/TYPE=(InnoDB|MyISAM)/ENGINE=$1/;
$line =~ s/;(\n?)\z/ CHARSET=utf8;$1/msx;
}
undef $statement;
}
print {$out} $line;
}
close $in
or WGDev::X::System->throw('Unable to run mysqldump');
return 1;
}
sub write_db_data {
my $self = shift;
my $out = shift;
my $wgd = $self->wgd;
my $dbh = $wgd->db->connect;
my $version = $wgd->version->database($dbh);
my %skip_data_tables = map { $_ => 1 } qw(
userSession userSessionScratch
webguiVersion userLoginLog
assetHistory cache
);
my @tables;
my $sth = $dbh->table_info( undef, undef, q{%}, undef );
while ( ( undef, undef, my $table ) = $sth->fetchrow_array ) {
next
if $skip_data_tables{$table};
my ($count)
= $dbh->selectrow_array(
'SELECT COUNT(*) FROM ' . $dbh->quote_identifier($table) );
next
if !$count;
push @tables, $table;
}
open my $in, q{-|}, 'mysqldump',
$wgd->db->command_line( '--no-create-info', '--compact',
'--disable-keys', sort @tables, )
or WGDev::X::System->throw('Unable to run mysqldump');
while ( my $line = <$in> ) {
$line =~ s{ /[*] !\d+ \s+ ([^*]+?) \s* [*]/; }{$1;}msx;
print {$out} $line;
}
close $in
or WGDev::X::System->throw('Unable to run mysqldump');
print {$out} 'INSERT INTO webguiVersion '
. '(webguiVersion,versionType,dateApplied) '
. "VALUES ('$version','Initial Install',UNIX_TIMESTAMP());\n";
return 1;
}
sub update_local_uploads {
my $self = shift;
my $wgd = $self->wgd;
$self->report('Loading uploads from site... ');
my $wg_uploads = File::Spec->catdir( $wgd->root, 'www', 'uploads' );
my $site_uploads = $wgd->config->get('uploadsPath');
WGDev::File->sync_dirs($site_uploads, $wg_uploads);
$self->report("Done\n");
return 1;
}
1;
=head1 SYNOPSIS
wgd build [-s] [-u]
=head1 DESCRIPTION
Uses the current database and uploads to build a new F<create.sql> and update
the local uploads directory. With no options, builds both the database
script and the uploads directory.
=head1 OPTIONS
=over 8
=item C<-s> C<--sql>
Make F<create.sql> based on current database contents
=item C<-u> C<--uploads>
Make uploads based on current site's uploads
=back
=method C<create_db_script>
Builds the F<create.sql> database script. This is done as a dump of the current
database structure and data, excluding the data from some tables.
=method C<update_local_uploads>
Updates the working directory's uploads from the current site. Files will be
deleted or created so the two match.
=cut

View file

@ -0,0 +1,93 @@
package WGDev::Command::Commands;
# ABSTRACT: List WGDev sub-commands
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::Command;
use WGDev::Help;
use WGDev::X ();
sub needs_root {
return;
}
sub process {
my $self = shift;
return $self->help;
}
sub help {
my $class = shift;
print "Sub-commands available:\n";
my %abstracts = $class->command_abstracts;
my @commands = sort keys %abstracts;
@commands = (
'intro',
'commands',
'help',
undef,
grep { $_ ne 'intro' && $_ ne 'commands' && $_ ne 'help' } @commands,
);
for my $command (@commands) {
if ( !defined $command ) {
print "\n";
next;
}
my $command_abstract = $abstracts{$command} || '(external command)';
printf " %-15s - %s\n", $command, $command_abstract;
}
return 1;
}
sub command_abstracts {
my $class = shift;
my %abstracts = map { $_ => undef } WGDev::Command->command_list;
require Pod::PlainText;
my $parser = Pod::PlainText->new( indent => 0, width => 1000 );
$parser->select('NAME');
for my $command ( keys %abstracts ) {
my $command_module
= eval { WGDev::Command->get_command_module($command) };
next
if !$command_module;
my $pod = WGDev::Help::package_pod($command_module);
my $formatted_pod = q{};
open my $pod_in, '<', \$pod
or WGDev::X::IO->throw;
open my $pod_out, '>', \$formatted_pod
or WGDev::X::IO->throw;
$parser->parse_from_filehandle( $pod_in, $pod_out );
close $pod_in or WGDev::X::IO->throw;
close $pod_out or WGDev::X::IO->throw;
if ( $formatted_pod =~ /^ [:\w]+ \s* - \s* (.+?) \s* $/msx ) {
$abstracts{$command} = $1;
}
}
return %abstracts;
}
1;
=head1 SYNOPSIS
wgd commands
=head1 DESCRIPTION
Provides an overview of the available WGDev commands.
=head1 OPTIONS
None
=method C<command_abstracts>
A class method which returns a hash with keys of the available
commands and values of the module abstract extracted from POD.
=cut

128
lib/WGDev/Command/Config.pm Normal file
View file

@ -0,0 +1,128 @@
package WGDev::Command::Config;
# ABSTRACT: Report or set WGDev configuration parameters
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev ();
use WGDev::X ();
use WGDev::Command ();
sub needs_root {
return;
}
sub config_options {
return qw(
struct|s
);
}
sub config_parse_options { return qw(gnu_getopt pass_through) }
sub process {
my $self = shift;
my $wgd = $self->wgd;
my @args = $self->arguments;
if ( !@args ) {
my $usage = $self->usage(0);
warn $usage;
return;
}
my ( $config_param, $value ) = @args;
my @config_path = split /[.]/msx, $config_param;
if ( defined $value ) {
if ( $value =~ s/\A@//msx ) {
my $file = $value;
my $fh;
if ( $file eq q{-} ) {
open $fh, '<&=', \*STDIN
or WGDev::X::IO::Read->throw;
}
else {
open $fh, '<', $file
or WGDev::X::IO::Read->throw( path => $file );
}
$value = do { local $/; <$fh> };
close $fh
or WGDev::X::IO::Read->throw( path => $file );
}
if ( $self->option('struct') ) {
$value =~ s/\A \s* ( [[{] ) /--- $1/msx;
$value .= "\n";
eval {
$value = WGDev::yaml_decode($value);
1;
} or WGDev::X->throw('Invalid or unsupported format.');
}
}
my $param
= $wgd->wgd_config( \@config_path, defined $value ? $value : () );
if ( defined $value && defined $param ) {
$wgd->write_wgd_config;
return 1;
}
if ( ref $param ) {
$param = WGDev::yaml_encode($param);
$param =~ s/\A---(?:\Q {}\E)?\n?//msx;
}
elsif ( !defined $param ) {
return 0;
}
$param =~ s/\n?\z/\n/msx;
print $param;
return 1;
}
1;
=head1 SYNOPSIS
wgd config [--struct] <config path> [<value>]
=head1 DESCRIPTION
Report or set WGDev configuration parameters.
=head1 OPTIONS
=over 8
=item C<-s> C<--struct>
When setting a config value, specifies that the value should be treated as a
data structure formatted as YAML or JSON.
=item C<< <config path> >>
Path of the the config variable to retrieve. Sub-level options are specified
as a period separated list of keys. Complex options will be returned formatted
as YAML.
=item C<< <value> >>
The value to set the config option to.
=back
=head1 CONFIGURATION
The WGDev config file is a JSON formatted file existing as either
F</etc/wgdevcfg> or F<.wgdevcfg> in the current user's home directory.
A simple config file looks like:
{
"command" : {
"webgui_root" : "/data/WebGUI",
"webgui_config" : "dev.localhost.localdomain.conf"
}
}
=cut

115
lib/WGDev/Command/Db.pm Normal file
View file

@ -0,0 +1,115 @@
package WGDev::Command::Db;
# ABSTRACT: Connect to database with the MySQL client
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::X ();
sub config_options {
return qw(
print|p
dump|d:s
load|l=s
clear|c
show
);
}
sub config_parse_options { return qw(gnu_getopt pass_through) }
sub process {
my $self = shift;
my $db = $self->wgd->db;
my @command_line = $db->command_line( $self->arguments );
if ( ( defined $self->option('print') || 0 )
+ ( defined $self->option('dump') || 0 )
+ ( defined $self->option('load') || 0 )
+ ( defined $self->option('clear') || 0 ) > 1 )
{
WGDev::X->throw('Multiple database operations specified!');
}
if ( $self->option('print') ) {
print join q{ }, map {"$_"} @command_line;
return 1;
}
if ( $self->option('clear') ) {
$db->clear;
return 1;
}
if ( defined $self->option('load') ) {
if ( $self->option('load') && $self->option('load') ne q{-} ) {
$db->clear;
$db->load( $self->option('load') );
return 1;
}
}
if ( defined $self->option('dump') ) {
if ( $self->option('dump') && $self->option('dump') ne q{-} ) {
$db->dump( $self->option('dump') );
return 1;
}
else {
my $return = system {'mysqldump'} 'mysqldump', @command_line;
return $return ? 0 : 1;
}
}
if ( defined $self->option('show') ) {
my $return = system {'mysqlshow'} 'mysqlshow', @command_line;
return $return ? 0 : 1;
}
my $return = system {'mysql'} 'mysql', @command_line;
return $return ? 0 : 1;
}
1;
=head1 SYNOPSIS
wgd db [-p | -d | -l | -c | --show] [mysql options]
=head1 DESCRIPTION
Opens the C<mysql> client to your WebGUI database, loads or dumps a database
script, or displays database information, or clears a database's contents.
=head1 OPTIONS
Any arguments not recognized will be passed through to the C<mysql> or
C<mysqldump> commands as applicable.
=over 8
=item C<-p> C<--print>
Prints out the command options that would be passed to C<mysql>
=item C<-d> C<--dump=>
Dumps the database as an SQL script. If a file is specified, dumps to that
file. Otherwise, dumps to standard out.
=item C<-l> C<--load=>
Loads a database script into the database. Database script must be specified.
=item C<-c> C<--clear>
Clears the database, removing all tables.
=item C<--show>
Shows database information via C<mysqlshow>.
For example, to display a summary of the number of columns and rows in each table,
use C<mysqlshow>'s C<--count> option:
wgd db --show --count
=back
=cut

184
lib/WGDev/Command/Dist.pm Normal file
View file

@ -0,0 +1,184 @@
package WGDev::Command::Dist;
# ABSTRACT: Create a distribution file for WebGUI
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use File::Spec ();
sub config_options {
return (
shift->SUPER::config_options, qw(
buildDir|b=s
) );
}
sub needs_config {
return;
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
require File::Temp;
File::Temp->VERSION(0.19); ##no critic (ProhibitMagicNumbers)
require File::Copy;
require Cwd;
my ( $version, $status ) = $wgd->version->module;
my $build_dir = $self->option('buildDir');
my $build_root;
if ($build_dir) {
$build_root = $build_dir;
mkdir $build_root;
}
if ( $build_root && !-e $build_root ) {
$build_root = File::Temp->newdir;
}
my $build_webgui = File::Spec->catdir( $build_root, 'WebGUI' );
my $build_docs = File::Spec->catdir( $build_root, 'api' );
my $cwd = Cwd::cwd();
mkdir $build_webgui;
$self->export_files($build_webgui);
my $inst_dir = $build_dir || $cwd;
if ( !fork ) {
chdir $build_root;
exec 'tar', 'czf',
File::Spec->catfile( $inst_dir,
"webgui-$version-$status.tar.gz" ),
'WebGUI';
}
wait;
mkdir $build_docs;
$self->generate_docs($build_docs);
if ( !fork ) {
chdir $build_root;
exec 'tar', 'czf',
File::Spec->catfile(
$inst_dir, "webgui-api-$version-$status.tar.gz"
),
'api';
}
wait;
return 1;
}
sub export_files {
my $self = shift;
my $to_root = shift;
my $from = $self->wgd->root;
if ( -e File::Spec->catdir( $from, '.git' ) ) {
system 'git', '--git-dir=' . File::Spec->catdir( $from, '.git' ),
'checkout-index', '-a', '--prefix=' . $to_root . q{/};
}
elsif ( -e File::Spec->catdir( $from, '.svn' ) ) {
system 'svn', 'export', $from, $to_root;
}
else {
system 'cp', '-r', $from, $to_root;
}
for my $file (
[ 'docs', 'previousVersion.sql' ],
[ 'etc', '*.conf' ],
[ 'sbin', 'preload.custom' ],
[ 'sbin', 'preload.exclude' ] )
{
my $file_path = File::Spec->catfile( $to_root, @{$file} );
for my $file ( glob $file_path ) {
unlink $file;
}
}
return $to_root;
}
sub generate_docs {
my $self = shift;
my $to_root = shift;
my $from = $self->wgd->root;
require File::Find;
require File::Path;
require Pod::Html;
require File::Temp;
File::Temp->VERSION(0.19); ##no critic (ProhibitMagicNumbers)
my $code_dir = File::Spec->catdir( $from, 'lib', 'WebGUI' );
my $temp_dir = File::Temp->newdir;
File::Find::find( {
no_chdir => 1,
wanted => sub {
no warnings 'once';
my $code_file = $File::Find::name;
return
if -d $code_file;
my $doc_file = $code_file;
return
if $doc_file =~ /\b\QOperation.pm\E$/msx;
return
if $doc_file !~ s/\Q.pm\E$/.html/msx;
$doc_file = File::Spec->rel2abs(
File::Spec->abs2rel( $doc_file, $code_dir ), $to_root );
my $directory = File::Spec->catpath(
( File::Spec->splitpath($doc_file) )[ 0, 1 ] );
File::Path::mkpath($directory);
Pod::Html::pod2html(
'--quiet',
'--noindex',
'--infile=' . $code_file,
'--outfile=' . $doc_file,
'--cachedir=' . $temp_dir,
);
},
},
$code_dir
);
return $to_root;
}
1;
=head1 SYNOPSIS
wgd dist [-c] [-d] [-b /data/builds]
=head1 DESCRIPTION
Generates distribution files containing WebGUI or the WebGUI API.
=head1 OPTIONS
By default, generates both a code and API documentation package.
=over 8
=item C<-c> C<--code>
Generates a code distribution
=item C<-d> C<--documentation>
Generates an API documentation distribution
=item C<-b> C<--buildDir>
Install the directories and tarballs in a different location. If no build directory
is specified, it will create a temp file.
=back
=method C<export_files ( $directory )>
Exports the WebGUI root directory, excluding common site specific files, to
the specified directory.
=method C<generate_docs ( $directory )>
Generate API documentation for WebGUI using Pod::Html in the specified
directory.
=cut

224
lib/WGDev/Command/Edit.pm Normal file
View file

@ -0,0 +1,224 @@
package WGDev::Command::Edit;
# ABSTRACT: Edits assets by URL
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev ();
sub config_options {
return qw(
command=s
tree=s@
class=s@
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my @files = $self->export_asset_data;
if ( !@files ) {
WGDev::X->throw('No assets to edit!');
}
## no critic (ProhibitParensWithBuiltins)
my $command = $self->option('command') || $ENV{EDITOR} || 'vi';
system join( q{ }, $command, map { $_->{filename} } @files );
my $output_format = "%-8s: %-30s (%22s) %s\n";
my $version_tag;
for my $file (@files) {
open my $fh, '<:encoding(UTF-8)', $file->{filename} or next;
my $asset_text = do { local $/; <$fh> };
close $fh or next;
unlink $file->{filename};
if ( $asset_text eq $file->{text} ) {
printf $output_format,
'Skipping', ( $file->{url} || $file->{title} ),
( $file->{asset_id} || q{} ), $file->{title};
next;
}
$version_tag ||= do {
require WebGUI::VersionTag;
my $vt = WebGUI::VersionTag->getWorking( $wgd->session );
$vt->set( { name => 'WGDev Asset Editor' } );
$vt;
};
my $asset_data = $wgd->asset->deserialize($asset_text);
my $asset;
my $parent;
if ( $asset_data->{parent} ) {
$parent = eval { $wgd->asset->find( $asset_data->{parent} ) };
}
if ( $file->{asset_id} ) {
$asset = $wgd->asset->by_id( $file->{asset_id}, undef,
$file->{revision} );
$asset = $asset->addRevision(
$asset_data,
undef,
{
skipAutoCommitWorkflows => 1,
skipNotification => 1,
} );
if ($parent) {
$asset->setParent($parent);
}
}
else {
$parent ||= $wgd->asset->import_node;
my $asset_id = $asset_data->{assetId};
$asset = $parent->addChild(
$asset_data,
$asset_id,
undef,
{
skipAutoCommitWorkflows => 1,
skipNotification => 1,
} );
}
printf $output_format, ( $file->{asset_id} ? 'Updating' : 'Adding' ),
$asset->get('url'), $asset->getId, $asset->get('title');
}
if ($version_tag) {
$version_tag->commit;
}
return 1;
}
sub export_asset_data {
my $self = shift;
my $wgd = $self->wgd;
my @files;
for my $asset_spec ( $self->arguments ) {
my $file_data = eval { $self->write_temp($asset_spec) };
if ( !$file_data ) {
warn $@;
next;
}
push @files, $file_data;
}
if ( $self->option('tree') ) {
for my $parent_spec ( @{ $self->option('tree') } ) {
my $parent = $wgd->asset->find($parent_spec) || do {
warn "$parent_spec is not a valid asset!\n";
next;
};
my $options = {};
if ( $self->option('class') ) {
my @classes = @{ $self->option('class') };
for (@classes) {
s/^(?:(?:WebGUI::Asset)?::)?/WebGUI::Asset::/msx;
}
$options->{includeOnlyClasses} = \@classes;
}
my $assets
= $parent->getLineage( [qw(self descendants)], $options );
for my $asset_id ( @{$assets} ) {
my $file_data = $self->write_temp($asset_id);
if ( !$file_data ) {
next;
}
push @files, $file_data;
}
}
}
return @files;
}
sub write_temp {
my $self = shift;
my $asset = shift;
require File::Temp;
my $wgd_asset = $self->wgd->asset;
if ( !ref $asset ) {
$asset = eval { $wgd_asset->find($asset) }
|| eval { scalar $wgd_asset->validate_class($asset) };
if ( !$asset ) {
die $@;
}
}
my $short_class = ref $asset || $asset;
$short_class =~ s/^WebGUI::Asset:://msx;
my ( $fh, $filename ) = File::Temp::tempfile();
binmode $fh, ':encoding(UTF-8)';
my $asset_text = $self->wgd->asset->serialize($asset);
print {$fh} $asset_text;
close $fh or return;
return {
filename => $filename,
text => $asset_text,
class => ref $asset || $asset,
ref $asset
? (
asset_id => $asset->getId,
url => $asset->get('url'),
title => $asset->get('title'),
)
: ( title => 'New ' . $short_class, ),
};
}
1;
=head1 SYNOPSIS
wgd edit [--command=<command>] <asset> [<asset> ...]
wgd edit --tree=<asset> [--tree=<asset> ...] [--class=<class> ...]
=head1 DESCRIPTION
Exports asset to temporary files, then opens them in your prefered editor.
If modifications are made, the assets are updated.
=head1 OPTIONS
=over 8
=item C<--command=>
Command to be executed. If not specified, uses the EDITOR environment
variable. If that is not specified, uses C<$EDITOR> or C<vi>.
=item C<< <asset> >>
Either an asset URL, ID, or class name. As many can be specified as desired.
Prepending with a slash will force it to be interpreted as a URL. Class names
specified will be opened with a skeleton for the asset type.
=item C<--tree=>
Will open specified asset and all descendants in editor. Can be specified
multiple times.
=item C<--class=>
Only used with --tree option. Limits exported assets to specified classes.
Can be specified as a full (C<WebGUI::Asset::Template>) or abbreviated
(C<Template>) class name.
=back
=method C<export_asset_data>
For each item in C<arguments>, exports the asset serialized to text to a
temporary file. Also follows the C<--tree> option. Returns an array of
hash references with information about the assets and exported files.
=method C<write_temp ( $asset_or_class )>
Accepts an asset or a class name and exports it serialized as a text file.
Returns a hash reference of information about the file ans asset.
=cut

101
lib/WGDev/Command/Export.pm Normal file
View file

@ -0,0 +1,101 @@
package WGDev::Command::Export;
# ABSTRACT: Exports assets to files
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::X ();
sub config_options {
return qw(
stdout
);
}
sub process {
my $self = shift;
my $wgd_asset = $self->wgd->asset;
for my $asset_spec ( $self->arguments ) {
my $asset = eval { $wgd_asset->find($asset_spec) }
|| eval { $wgd_asset->validate_class($asset_spec) };
if ( !$asset ) {
warn $@;
next;
}
my $asset_text = $self->wgd->asset->serialize($asset);
if ( $self->option('stdout') ) {
print $asset_text;
}
else {
my $filename = $self->export_filename($asset);
print "Writing $filename...\n";
open my $fh, '>', $filename
or WGDev::X::IO::Write->throw( path => $filename );
print {$fh} $asset_text;
close $fh
or WGDev::X::IO::Write->throw( path => $filename );
}
}
return 1;
}
sub export_filename {
my $self = shift;
my $asset = shift;
my $class = ref $asset || $asset;
my $short_class = $class;
$short_class =~ s/.*:://msx;
my $extension = lc $short_class;
$extension =~ tr/aeiouy//d;
$extension =~ tr/a-z//s;
my $filename;
if ( ref $asset ) {
$filename = ( split m{/}msx, $asset->get('url') )[-1];
}
else {
$filename = 'new-' . lc $short_class;
}
$filename .= ".$extension";
return $filename;
}
1;
=head1 SYNOPSIS
wgd export [--stdout] <asset> [<asset> ...]
=head1 DESCRIPTION
Exports asset to files.
=head1 OPTIONS
=over 8
=item C<--stdout>
Exports to standard out instead of a file. This only makes sense with a single asset specified.
=item C<< <asset> >>
Either an asset URL, ID, class name. As many can be specified as desired.
Prepending with a slash will force it to be interpreted as a URL. Asset
classes will generate skeletons of export files for the given class.
=back
=method C<export_filename ( $asset_or_class )>
Calculates the file name to export an asset as. Accepts a parameter of the
asset object or an asset class name. The file name will be the last portion
of the asset's URL, with an extension based on the asset's class name. If
provided only a class name, the file name will also be based on the class
name.
=cut

View file

@ -0,0 +1,119 @@
package WGDev::Command::Export::Branch;
# ABSTRACT: Export a branch of assets
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base::Verbosity);
use File::Spec ();
use Cwd ();
use constant LINEAGE_LEVEL_LENGTH => 6;
sub config_options {
return (
shift->SUPER::config_options, qw(
to|t=s
hier!
) );
}
sub parse_params {
my $self = shift;
my $result = $self->SUPER::parse_params(@_);
$self->set_option_default( 'hier', 1 );
return $result;
}
sub process {
my $self = shift;
require File::Path;
my $wgd_asset = $self->wgd->asset;
my $base_dir = $self->option('to') || Cwd::cwd;
my $heir = $self->option('hier');
for my $asset_spec ( $self->arguments ) {
my $base_asset = eval { $wgd_asset->find($asset_spec) };
if ( !$base_asset ) {
warn $@;
next;
}
$self->report( 'Exporting "' . $base_asset->get('title') . "...\n" );
if ( $self->verbosity ) {
$self->tab_level(1);
}
my $iter
= $base_asset->getLineageIterator( [ 'self', 'descendants' ] );
my $base_depth
= length( $base_asset->get('lineage') ) / LINEAGE_LEVEL_LENGTH;
while ( my $asset = $iter->() ) {
my @url_segments;
if ($heir) {
my $parent = $asset;
my $depth
= length( $asset->get('lineage') ) / LINEAGE_LEVEL_LENGTH;
while (1) {
my $url_part = $parent->get('url');
$url_part =~ s{.*/}{}msx;
unshift @url_segments, $url_part;
last
if --$depth < $base_depth;
$parent = $parent->getParent;
}
}
else {
@url_segments = split m{/}msx, $asset->get('url');
}
my $extension = $wgd_asset->export_extension($asset);
my $filename = ( pop @url_segments ) . ".$extension";
$self->report( 0,
File::Spec->catfile( @url_segments, $filename ) . "\n" );
my $dir = File::Spec->catdir( $base_dir, @url_segments );
my $full_path = File::Spec->catfile( $dir, $filename );
File::Path::mkpath($dir);
my $asset_text = $wgd_asset->serialize($asset);
open my $fh, '>', $full_path
or WGDev::X::IO::Write->throw( path => $full_path );
print {$fh} $asset_text;
close $fh
or WGDev::X::IO::Write->throw( path => $full_path );
}
if ( $self->verbosity ) {
$self->tab_level(-1);
}
$self->report("Done.\n");
}
return 1;
}
1;
=head1 SYNOPSIS
wgd export-branch [--no-hier] [--to=<output dir>] <asset> [<asset> ...]
=head1 DESCRIPTION
Exports a branch of assets as serialized files.
=head1 OPTIONS
=over 8
=item C<--[no-]hier>
Exports assets in a directories based on their hierarchy in the
asset tree. If not enabled, the serialized assets' location is
based directly on their URLs. Enabled by default.
=item C<-t> C<--to=>
Output directory to place the exported files in. If not specified,
files are placed in the current directory.
=back
=cut

View file

@ -0,0 +1,193 @@
package WGDev::Command::For::Each;
# ABSTRACT: Run command for each available config file
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->{commands} = [];
return $self;
}
sub needs_config {
return;
}
sub needs_root {
return 1;
}
sub config_options {
return (
shift->SUPER::config_options, qw(
exec|e=s
print|p:s
print0|0:s
wgd|c|w=s
force|f
) );
}
sub option_exec {
my $self = shift;
my ($command) = @_;
push @{ $self->{commands} },
command_exec => [$command];
}
sub option_print {
my $self = shift;
my $format = shift;
push @{ $self->{commands} },
command_print => [
format => $format,
];
}
sub option_print0 {
my $self = shift;
my $format = shift;
push @{ $self->{commands} },
command_print => [
separator => "\0",
format => $format,
];
}
sub option_wgd {
my $self = shift;
my ($command) = @_;
push @{ $self->{commands} },
command_wgd => [$command];
}
sub command_print {
my $self = shift;
my %options = @_;
my $separator = $options{separator} || "\n";
my $format = $options{format} || '%s';
$format .= $separator;
printf $format, $self->wgd->config_file;
return 1;
}
sub command_exec {
my $self = shift;
my $command = shift;
local %ENV = %ENV;
$self->wgd->set_environment(localized => 1);
system $command
and WGDev::X::System->throw('Error running shell command.');
return 1;
}
sub command_wgd {
my $self = shift;
my $command = shift;
my $wgd = $self->wgd;
require Text::ParseWords;
my @command_line = (
'-R' . $wgd->root,
'-F' . $wgd->config_file,
Text::ParseWords::shellwords($command),
);
return WGDev::Command->run(@command_line);
}
sub process {
my $self = shift;
my @commands = @{ $self->{commands} };
my $force = $self->option('force');
if (! @commands ) {
@commands = (command_print => []);
}
my $root = $self->wgd->root;
SITES: for my $config ( $self->wgd->list_site_configs ) {
my $wgd = eval { WGDev->new( $root, $config ) };
if ( $wgd ) {
##no critic (ProhibitCStyleForLoops ProhibitLocalVars)
local $self->{wgd} = $wgd;
COMMANDS: for (my $i = 0; $i <= $#commands; $i += 2) {
my $command = $commands[$i];
my @params = @{ $commands[$i + 1] };
my $success = eval {
$self->$command(@params) || 1;
};
if ( $success ) {
# nothing
}
elsif ( $force ) {
warn $@;
}
else {
WGDev::X->inflate($@);
}
}
}
elsif ($force) {
warn $@;
}
else {
WGDev::X->inflate($@);
}
}
return 1;
}
1;
=head1 SYNOPSIS
wgd for-each [ --print0 | --exec=command ] [ -f ]
=head1 DESCRIPTION
Runs a command for each available WebGUI config file. By default,
the names of the config files will be output.
=head1 OPTIONS
=over 8
=item C<-f> C<--force>
Continue processing config files if there is an error
=item C<-0> C<--print0[=format]>
Prints the config file name followed by an ASCII C<NUL> character
instead of a carriage return.
An optional L<perlfunc/sprintf> formatting string can be specified.
=item C<-p> C<--print[=format]>
Prints the config file name. This is the default option if no other
options are specified.
An optional L<perlfunc/sprintf> formatting string can be specified.
=item C<-e> C<--exec=>
Runs the given command using the shell for each config file. The
WEBGUI_ROOT and WEBGUI_CONFIG environment variables will be set
while this command is run.
=item C<-w> C<-c> C<--wgd=>
Runs the given WGDev command for each config file.
=back
=cut

115
lib/WGDev/Command/Group.pm Normal file
View file

@ -0,0 +1,115 @@
package WGDev::Command::Group;
# ABSTRACT: Utilities for manipulating WebGUI Groups
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::X ();
sub config_options {
return qw(
list|l
format|f=s
long
hidden
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my $session = $wgd->session();
if ( $self->option('list') ) {
my $format = $self->option('format');
if ( $self->option('long') ) {
$format
= 'Name: %groupName% %%n Id: %groupId% %%n Description: %description% %%n';
}
elsif ( !$format ) {
$format = '%groupName%';
}
my $show_in_forms = $self->option('hidden');
my $group_ids = $session->db->buildArrayRef(
'select groupId from groups order by groupName');
for my $group_id ( @{$group_ids} ) {
my $group = WebGUI::Group->new( $session, $group_id );
if ( !$group ) {
warn "Unable to instantiate group via groupId: $group_id";
next;
}
next if !$show_in_forms && !$group->showInForms;
my $output = $self->format_output( $format, $group );
print $output . "\n";
}
}
}
sub format_output {
my ( $self, $format, $group ) = @_;
$format =~ s/%%n/\n/msxg;
{
no warnings 'uninitialized';
$format =~ s{% (?: (\w+) (?: :(-?\d+) )? )? %}{
my $replace;
if ($1) {
$replace = $group->get($1);
if ($2) {
$replace = sprintf('%*2$s', $replace, $2);
}
}
else {
$replace = '%';
}
$replace;
}msxeg;
}
return $format;
}
1;
=head1 SYNOPSIS
wgd group [--list [--long] [--hidden]]
=head1 DESCRIPTION
Utilities for manipulating WebGUI Groups
=head1 OPTIONS
=over 8
=item C<-l> C<--list>
List groups. This is currently the only supported action.
=item C<--long>
Use long list format, which includes group name, ID, and description.
=item C<-f> C<--format=>
Use arbitrary formatting. Format looks like C<%description:30%>, where 'C<description>' is
the field to display, and 30 is the length to left pad/cut to. Negative
lengths can be specified for right padding. Percent signs can be included by
using C<%%>. Newlines can be included by using C<%%n>
=item C<--hidden>
Include groups that are normally hidden from WebGUI forms.
=back
=method C<format_output ( $format, $group )>
Returns the formatted information about a group. C<$format> is
the format to output as specified in the L<format option|/-f>.
=cut

70
lib/WGDev/Command/Guid.pm Normal file
View file

@ -0,0 +1,70 @@
package WGDev::Command::Guid;
# ABSTRACT: Generates GUIDs via WebGUI's $session->id->generate API
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub config_options {
return qw(
number|n=i
dashes!
toHex
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my $session = $wgd->session();
my $id = $session->id;
if ( $self->option('toHex') ) {
foreach my $guid ( $self->arguments ) {
printf "%s : %s\n", $guid, $id->toHex($guid);
}
return;
}
my $number = $self->option('number') || 1;
$self->set_option_default( dashes => 1 );
for ( 1 .. $number ) {
my $guid = $id->generate();
if ( !$self->option('dashes') && $guid =~ /[-_]/msx ) {
redo;
}
print "$guid\n";
}
return 1;
}
1;
=head1 SYNOPSIS
wgd guid [-n <quantity>] [--no-dashes]
=head1 DESCRIPTION
Generates GUIDs via WebGUI's C<$session->id->generate> API. Optionally
excludes GUIDs with dashes (for easy double-click copy/pasting).
=head1 OPTIONS
=over 8
=item C<-n> C<--number>
Number of GUIDs to generate. Defaults to 1.
=item C<--[no-]dashes>
Whether or not to filter GUIDs containing dashes (for easy double-click copy/pasting)
=back
=cut

78
lib/WGDev/Command/Help.pm Normal file
View file

@ -0,0 +1,78 @@
package WGDev::Command::Help;
# ABSTRACT: Displays C<perldoc> help for WGDev command
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::Command ();
use WGDev::X ();
sub needs_root {
return;
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my ($command) = $self->arguments;
if ( !defined $command ) {
print WGDev::Command->usage(1);
return 1;
}
my $command_module;
if ( $command eq 'wgd' ) {
$command_module = 'WGDev::Command';
}
else {
$command_module = WGDev::Command->get_command_module($command);
}
if ( !$command_module ) {
WGDev::X::CommandLine::BadCommand->throw(
usage => $self->usage,
command_name => $command,
);
}
if ( $command_module->can('help') ) {
return $command_module->help;
}
require WGDev::Help;
WGDev::Help::package_perldoc( $command_module,
'!AUTHOR|LICENSE|METHODS|SUBROUTINES' );
return 1;
}
1;
=head1 SYNOPSIS
wgd help <command>
=head1 DESCRIPTION
Displays C<perldoc> page for WGDev command.
More or less equivalent to running
wgd command --help
Except that the help message is displayed via Pod::Perldoc
=head1 OPTIONS
=over 8
=item C<< <command> >>
The sub-command to display help information about.
=back
=cut

View file

@ -0,0 +1,79 @@
package WGDev::Command::Import;
# ABSTRACT: Import assets from files
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub process {
my $self = shift;
my $wgd_asset = $self->wgd->asset;
my $version_tag;
for my $asset_file ( $self->arguments ) {
open my $fh, '<:encoding(UTF-8)', $asset_file or next;
my $asset_text = do { local $/; <$fh> };
close $fh or next;
$version_tag ||= do {
require WebGUI::VersionTag;
my $vt = WebGUI::VersionTag->getWorking( $self->wgd->session );
$vt->set( { name => 'WGDev Asset Import' } );
$vt;
};
my $asset_data = $wgd_asset->deserialize($asset_text);
my $parent;
if ( $asset_data->{parent} ) {
$parent = eval { $wgd_asset->find( $asset_data->{parent} ) };
}
my $asset;
my $mode;
if ( eval { $asset = $wgd_asset->by_id( $asset_data->{assetId} ) } ) {
$mode = 'Updating';
$asset->addRevision( $asset_data, undef,
{ skipAutoCommitWorkflows => 1, skipNotification => 1 } );
if ( $asset_data->{parent} ) {
if ($parent) {
$asset->setParent($parent);
}
}
}
else {
$mode = 'Adding';
$parent ||= $wgd_asset->import_node;
$asset = $parent->addChild( $asset_data, $asset_data->{assetId},
undef,
{ skipAutoCommitWorkflows => 1, skipNotification => 1 } );
}
printf "%8s: %-30s (%22s) %s\n", $mode,
$asset->get('url'), $asset->getId, $asset->get('title');
}
if ($version_tag) {
$version_tag->commit;
}
return 1;
}
1;
=head1 SYNOPSIS
wgd import <asset file> [<asset file> ...]
=head1 DESCRIPTION
Imports asset from files.
=head1 OPTIONS
=over 8
=item C<< <asset file> >>
File to import.
=back
=cut

108
lib/WGDev/Command/Intro.pm Normal file
View file

@ -0,0 +1,108 @@
package WGDev::Command::Intro;
# ABSTRACT: Introduction to WGDev
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub needs_root {
return;
}
sub process {
my $self = shift;
return $self->help;
}
1;
=head1 SYNOPSIS
wgd edit default_article
wgd package home
wgd reset --build
wgd reset --dev
wgd db
=head1 DESCRIPTION
WGDev provides a variety of commands useful for WebGUI developers.
=head1 GETTING STARTED
The first step in using WGDev is getting it to find your WebGUI
root directory and site config file. For this, you can either use
the C<WEBGUI_ROOT> and C<WEBGUI_CONFIG>/C<WEBGUI_SITENAME> environment
variables, setting the C<command.webgui_root> and
C<command.webgui_config>/C<command.webgui_sitename> options via the
L<config command|WGDev::Command::Config>, using command line
parameters (see C<wgd help>), or (for the root path) relying on
auto-detection.
Auto-detection works by searching upward from the current directory
for a valid WebGUI directory. The config file cannot be detected
and must be specified.
The WebGUI config file can be specified relative to the current
directory, relative to WebGUI's etc directory, or as an absolute
path.
Once you have the root and config file set or otherwise specified,
you can use any of the WGDev commands.
=head1 GETTING HELP
A summary of a command's options is available by running the command
with the C<--help> option. Full documentation is available using
the C<wgd help> command. A full list of available commands is
available by running C<wgd commands>.
=head1 SPECIFYING ASSETS
When specifying assets as parameters to commands, either an asset
URL or an asset ID can be specified. Some commands will also accept
a class name, treating it as an new asset of that type.
=head1 COMMON COMMANDS
=head2 C<< wgd edit <asset> >>
Edits the specified asset in your prefered text editor. When you
exit the editor, the asset on the WebGUI site will be updated with
the new data. Multiple assets can be specified.
=head2 C<< wgd package <asset> >>
The package command will generate a package for asset specified.
Additionally, the --import option allows you to import package
files, and --upgrade will export a package and put it into the correct
package directory for the next WebGUI release. Multiple assets can
be specified.
=head2 C<wgd reset --dev>
Resets a site to its defaults and sets it up for development. The
site started is disabled, leaving the admin login with the default
password of C<123qwe>. Additionally, all of the default example
content is cleared from the site giving you a blank slate to work
from.
=head2 C<wgd reset --build>
Resets a site it its defaults and prepares it to generate a site
creation script. The site starter is enabled, and old version tags
and revisions of content are cleaned up.
=head2 C<wgd db>
Starts the C<mysql> client in the site's database, using the login
information from the site config file.
=head2 C<< wgd export <asset> >>
Exports assets to files. You can export to standard out by using
the C<--stdout> option. Multiple assets can be specified.
=cut

224
lib/WGDev/Command/Ls.pm Normal file
View file

@ -0,0 +1,224 @@
package WGDev::Command::Ls;
# ABSTRACT: List WebGUI assets
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub config_options {
return qw(
format|f=s
long|l
recursive|r
excludeClass=s@
includeOnlyClass=s@
limit=n
isa=s
filter=s
);
}
sub option_filter {
my $self = shift;
my $filter = shift;
my ( $filter_prop, $filter_sense, $filter_match )
= $filter =~ m{%(\w+)% \s* ([~!])~ \s* (.*)}msx;
if ( !defined $filter_prop
|| !defined $filter_sense
|| !defined $filter_match )
{
WGDev::X->throw("Invalid filter specified: $filter");
}
if ( $filter_match =~ m{\A/(.*)/\Z}msx ) {
eval { $filter_match = qr/$1/msx; }
|| WGDev::X->throw(
"Specified filter is not a valid regular expression: $1");
}
else {
$filter_match = qr/\A\Q$filter_match\E\z/msx;
}
$self->{filter_property} = $filter_prop;
$self->{filter_sense} = $filter_sense eq q{~};
$self->{filter_match} = $filter_match;
return;
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my $format = $self->option('format');
if ( $self->option('long') ) {
$format = '%assetId% %url:-35% %title%';
}
elsif ( !$format ) {
$format = '%url%';
}
my $relatives = $self->option('recursive') ? 'descendants' : 'children';
my @parents = $self->arguments;
my $show_header = @parents > 1;
my $exclude_classes = $self->option('excludeClass');
my $include_only_classes = $self->option('includeOnlyClass');
my $limit = $self->option('limit');
my $isa = $self->option('isa');
my $error;
PARENT:
while ( my $parent = shift @parents ) {
my $asset;
if ( !eval { $asset = $wgd->asset->find($parent) } ) {
warn "wgd ls: $parent: No such asset\n";
$error++;
next;
}
if ($show_header) {
print "$parent:\n";
}
my $child_iter = $asset->getLineageIterator(
[$relatives],
{
$exclude_classes ? ( excludeClasses => $exclude_classes )
: (),
$include_only_classes
? ( includeOnlyClasses => $include_only_classes )
: (),
defined $limit
&& !defined $self->{filter_match} ? ( limit => $limit )
: (),
$isa ? ( isa => $isa ) : (),
} );
while ( my $child = $child_iter->() ) {
next
if !$self->pass_filter($child);
# Handle limit ourselves because smartmatch filtering happens
# *after* getLineage returns its results
last PARENT
if defined $limit && $limit-- <= 0;
my $output = $self->format_output( $format, $child );
print $output . "\n";
}
if (@parents) {
print "\n";
}
}
return (! $error);
}
sub pass_filter {
my ( $self, $asset ) = @_;
my $filter_prop = $self->{filter_property};
my $filter_sense = $self->{filter_sense};
my $filter_match = $self->{filter_match};
return 1
if !defined $filter_match;
{
no warnings 'uninitialized';
if ($filter_sense) {
return $asset->get($filter_prop) =~ $filter_match;
}
else {
return $asset->get($filter_prop) !~ $filter_match;
}
}
}
sub format_output {
my ( $self, $format, $asset ) = @_;
{
no warnings 'uninitialized';
$format =~ s{% (?: (\w+) (?: :(-?\d+) )? )? %}{
my $replace;
if ($1) {
$replace = $asset->get($1);
if ($2) {
$replace = sprintf('%*2$s', $replace, $2);
}
}
else {
$replace = '%';
}
$replace;
}msxeg;
}
return $format;
}
1;
=head1 SYNOPSIS
wgd ls [-l] [--format=<format>] [-r] <asset> [<asset> ...]
=head1 DESCRIPTION
Lists children of WebGUI assets
=head1 OPTIONS
=over 8
=item C<-l> C<--long>
Use long list format, which includes asset ID, URL, and title.
=item C<-f> C<--format=>
Use arbitrary formatting. Format looks like C<%url:30%>, where 'C<url>' is
the field to display, and 30 is the length to left pad/cut to. Negative
lengths can be specified for right padding. Percent signs can be included by
using C<%%>.
=item C<-r> C<--recursive>
Recursively list all descendants (by default we only list children).
=item C<--includeOnlyClass=>
Specify one or more times to limit the results to a certain set of asset classes.
=item C<--excludeClass=>
Specify one or more times to filter out certain asset class(es) from the results.
=item C<--limit=>
The maximum amount of entries to return
=item C<--isa=>
A class name where you can look for classes of a similar base class.
For example, if you're looking for Donations, Subscriptions, Products
and other subclasses of L<WebGUI::Asset::Sku>, then specify the
parameter C<--isa=WebGUI::Asset::Sku>.
=item C<--filter=>
Apply smart match filtering against the results. Format looks like
C<%url% ~~ smartmatch>, where C<url> is the field to filter against,
and C<smartmatch> is either a Perl regular expression such as
C</(?i:partial_match)/> or a string such as C<my_exact_match>.
=back
=method C<format_output ( $format, $asset )>
Returns the formatted information about an asset. C<$format> is
the format to output as specified in the L<format option|/-f>.
=method C<option_filter ( $filter )>
Takes a filter specification, verifies that it is specified properly, and saves it.
=method C<pass_filter ( $asset )>
Checks if a given asset passes the saved filter. Returns true or false.
=cut

168
lib/WGDev/Command/Mail.pm Normal file
View file

@ -0,0 +1,168 @@
package WGDev::Command::Mail;
# ABSTRACT: Sends emails via the WebGUI::Mail::Send API
use strict;
use warnings;
use 5.008008;
use Carp;
use parent qw(WGDev::Command::Base);
use WGDev::X ();
sub config_options {
return qw(
list|l
delete
processQueue
queue|q
toUser=s
toGroup=s
subject|s=s
from=s
cc=s
bcc=s
replyTo=s
returnPath=s
contentType=s
messageId=s
inReplyTo=s
isInbox
verbose|v
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my $verbose = $self->option('verbose');
# Handle special cases
if ( !$self->arguments ) {
my $dbh = $wgd->db->connect;
my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM mailQueue');
print "Mail queue has @{[ $count || 'no' ]} message(s).\n";
if ( $self->option('list') ) {
for my $message (
@{ $dbh->selectcol_arrayref('SELECT message FROM mailQueue') }
)
{
print $message . "\n";
}
}
elsif ( $self->option('delete') ) {
$dbh->do('DELETE FROM mailQueue');
print "Deleted all messages from mail queue.\n";
}
elsif ( $self->option('processQueue') ) {
my $WORKFLOW_ID = 'pbworkflow000000000007';
my $found
= $dbh->selectrow_array(
'SELECT count(*) FROM Workflow WHERE workflowId = ?',
{}, $WORKFLOW_ID, );
if ( !$found ) {
WGDev::X->throw(
q{The default "Send Queued Email Messages" Workflow was not found,}
. q{ unable to run.} );
}
require WebGUI::Workflow::Instance;
my $session = $wgd->session;
WebGUI::Workflow::Instance->create( $session,
{ workflowId => $WORKFLOW_ID, } )->start;
print
qq{Triggered Workflow, the mail queue should be being processed as we speak.\n};
}
return 1;
}
my $session = $wgd->session;
my $to = join q{,}, $self->arguments;
my $body;
while ( my $line = <STDIN> ) {
last if $line eq ".\n";
$body .= $line;
}
# We are going to pass pretty much all options into WebGUI::Mail::Send::create
my $options = $self->{options};
$options->{to} = $to;
# Pull out the non-api options (or short-hands)
if ( my $s = delete $options->{s} ) {
$options->{subject} = $s;
}
my $queue = delete $options->{q} || delete $options->{queue};
if ($verbose) {
print $queue ? 'Queueing' : 'Sending', " message:\n";
print $body;
print "Using the following options:\n";
print Data::Dumper::Dumper( $self->{options} );
print 'SMTP Server: ' . $session->setting->get('smtpServer') . "\n";
print "emailToLog: 1\n" if $session->config->get('emailToLog');
}
require WebGUI::Mail::Send;
my $msg = WebGUI::Mail::Send->create( $session, $options );
WGDev::X->throw('Unable to instantiate message') unless $msg;
$msg->addText($body);
my $status;
if ($queue) {
$status = $msg->queue;
}
else {
$status = $msg->send;
}
print "Status: $status\n" if $verbose;
return 1;
}
1;
=head1 SYNOPSIS
wgd mail
wgd mail -s test pat@patspam.com
=head1 DESCRIPTION
Sends emails via the L<WebGUI::Mail::Send> API
If run with no arguments, displays the number of messages currently
in the mail queue.
Accepts all options supported by
L<WebGUI::Mail::Send::create|WebGUI::Mail::Send/create>, plus the
following additional items:
=head1 OPTIONS
=over 8
=item C<-l> C<--list>
List (print) the raw contents of the mail queue.
=item C<--delete>
Delete the contents of the mail queue.
=item C<--processQueue>
Trigger the default "Send Queued Email Messages" Workflow. This
will send all of the messages in the mail queue.
=item C<-q> C<--queue>
Add the message to the queue rather than sending it immediately.
=item C<-s>
Short-hand for C<--subject>.
=back
=cut

View file

@ -0,0 +1,195 @@
package WGDev::Command::Optimize;
# ABSTRACT: Scans your site and suggests various optimizations
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base::Verbosity);
sub config_options {
return qw(
assets
macros
db
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
if ( $self->option('assets') ) {
$self->optimise_assets();
}
if ( $self->option('macros') ) {
$self->optimise_macros();
}
if ( $self->option('db') ) {
$self->optimise_db();
}
return 1;
}
sub optimise_assets {
my $self = shift;
my $wgd = $self->wgd;
my $session = $wgd->session();
my @assets;
for my $asset ( sort keys %{ $session->config->get('assets') } ) {
if (
!$session->db->quickScalar(
'select count(*) from asset where className = ?', [$asset] ) )
{
push @assets, $asset;
}
}
if (@assets) {
$self->report(
"The following Assets do not appear in your Asset table:\n");
for my $asset (@assets) {
$self->report("\t$asset\n");
}
my $config = $wgd->config_file();
my $message = <<"END_MESSAGE";
If you are sure any of these Assets are not being used on your site,
you can reduce memory usage by removing them from the "assets" section of
your site config file, which is located at:
\t$config
Keep in mind:
*) Some assets such as FilePile will not appear in your Assets table but
are still used to provide funcitonality (in the case of FilePile
providing a way for users to upload multiple Files).
END_MESSAGE
$self->report("$message");
}
return 1;
}
sub optimise_macros {
my $self = shift;
my $wgd = $self->wgd;
my $session = $wgd->session();
my @macros;
for my $macro ( sort keys %{ $session->config->get('macros') } ) {
if (
!$session->db->quickScalar(
'select count(*) from template where template like ? or template like ?',
[ "%^$macro;%", "%^$macro(%" ] ) )
{
push @macros, $macro;
}
}
if (@macros) {
my $macros = join q{}, map {"\t$_\n"} @macros;
my $config = $wgd->config_file();
my $message = <<"END_MESSAGE";
The following Macros do not appear in the template field of the template table:
$macros
If you are sure any of these Macros are not being used on your site,
you can reduce memory usage by removing them from the "macros" section of
your site config file, which is located at:
\t$config
Keep in mind:
*) Macros can be references from lots of places other then just Templates,
for example the mailFooter setting in the Settings table
END_MESSAGE
$self->report($message);
}
return 1;
}
use constant OPTIMIZE_TABLES_LIMIT => 10;
sub optimise_db {
my $self = shift;
my $wgd = $self->wgd;
my $session = $wgd->session();
my $sth = $session->db->read('show table status');
my @tables;
while ( my $r = $sth->hashRef ) {
push @tables, [ $r->{Name}, $r->{Data_length}, $r->{Rows} ];
}
$self->report("Top 10 Tables, sorted by Data_length\n");
my $ctr;
for my $table ( sort { $b->[1] <=> $a->[1] } @tables ) {
## no critic (ProhibitParensWithBuiltins)
$self->report( sprintf( "%10d\t%s\n", $table->[1], $table->[0] ) );
last
if ++$ctr == OPTIMIZE_TABLES_LIMIT;
}
$self->report("\n");
$self->report("Top 10 Tables, sorted by Rows\n");
$ctr = 0;
for my $table ( sort { $b->[2] <=> $a->[2] } @tables ) {
## no critic (ProhibitParensWithBuiltins)
$self->report( sprintf( "%10d\t%s\n", $table->[2], $table->[0] ) );
last
if ++$ctr == OPTIMIZE_TABLES_LIMIT;
}
$self->report("\n");
$self->report(<<'END_ADVICE');
To reduce row count, you may want to investigate deleting old/unused data.
To reduce row size, apart from deleting rows, you might want to investigate mysql's "optimize table" command.
END_ADVICE
return 1;
}
1;
=head1 SYNOPSIS
wgd optimize [--assets] [--macros]
=head1 DESCRIPTION
Scans your site and suggests various optimizations
=head1 OPTIONS
=over 8
=item C<--assets>
Suggests Assets that you might be able to disable to reduce memory consumption
=item C<--macros>
Suggests Macros that you might be able to disable to reduce memory consumption
=item C<--db>
Suggests database tables that may be able to be adjusted to increase speed.
=back
=method C<optimise_assets>
Suggests Assets that you might be able to disable to reduce memory consumption
=method C<optimise_macros>
Suggests Macros that you might be able to disable to reduce memory consumption
=method C<optimise_db>
Suggests database tables that may be able to be adjusted to increase speed.
=cut

View file

@ -0,0 +1,161 @@
package WGDev::Command::Package;
# ABSTRACT: Export assets for upgrade
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use File::Spec ();
use WGDev::X ();
sub config_options {
return qw(
import|i=s@
parent=s
overwrite
upgrade|u
to=s
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
require File::Copy;
if ( $self->arguments ) {
my $package_dir = $self->option('to') || q{.};
if ( $self->option('upgrade') ) {
my $version = $wgd->version->module;
my $wg8 = $version =~ /^8[.]/msx;
if ($wg8) {
require WebGUI::Paths;
my $old_version = $wgd->version->db_script;
$package_dir = File::Spec->catdir( WebGUI::Paths->upgrades,
$old_version . q{-} . $version );
}
else {
$package_dir = File::Spec->catdir( $wgd->root, 'docs', 'upgrades',
'packages-' . $wgd->version->module );
}
if ( !-d $package_dir ) {
mkdir $package_dir;
}
}
if ( !-d $package_dir ) {
WGDev::X::IO->throw(
error => 'Directory does not exist',
path => $package_dir
);
}
for my $asset_spec ( $self->arguments ) {
my $asset = eval { $wgd->asset->find($asset_spec) } || do {
warn "Unable to find asset $asset_spec!\n";
next;
};
my $storage = $asset->exportPackage;
my $filename = $storage->getFiles->[0];
my $filepath = $storage->getPath($filename);
File::Copy::copy( $filepath,
File::Spec->catfile( $package_dir, $filename ) );
printf "Building package %27s for %27s.\n", $filename,
$asset->get('title');
}
}
if ( $self->option('import') ) {
my $parent
= $self->option('parent')
? eval { $wgd->asset->find( $self->option('parent') ) }
: $wgd->asset->import_node;
if ( !$parent ) {
warn "Unable to find parent node!\n";
return 0;
}
require WebGUI::Storage;
require WebGUI::VersionTag;
my $version_tag = WebGUI::VersionTag->getWorking( $wgd->session );
$version_tag->set( { name => 'WGDev package import' } );
my $import_options = {};
if ($self->option('overwrite')) {
$import_options->{'overwriteLatest'} = 1;
}
for my $package ( @{ $self->option('import') } ) {
my $storage = WebGUI::Storage->createTemp( $wgd->session );
$storage->addFileFromFilesystem($package);
my $asset = $parent->importPackage($storage, $import_options);
if ( ! ref $asset ) {
# importPackage returns a string for errors (ugh)
WGDev::X::BadPackage->throw(
package => $package,
message => $asset,
);
}
elsif ( ! eval { $asset->isa('WebGUI::Asset') } ) {
# not an asset or an error? this shouldn't ever happen.
WGDev::X::BadPackage->throw(
package => $package,
message => 'Strange result from package import: '
. ref($asset),
);
}
print "Imported '$package' to " . $asset->get('url') . "\n";
}
$version_tag->commit;
}
return 1;
}
1;
=head1 SYNOPSIS
wgd package [--to=<dir>] [--upgrade] [<asset> ...]
wgd package [--parent=<asset>] [--import=<package file>]
=head1 DESCRIPTION
Exports or imports assets as packages, optionally placing them in the current
upgrade path.
=head1 OPTIONS
Assets specified as standalone arguments are exported as packages.
=over 8
=item C<-i> C<--import=>
Package file (or files) to import. Will be imported to the import node if no
other parent is specified.
=item C<--overwrite>
Forces the assets in this package to be the latest version on the
site. This option only works in conjunction with C<--import> and
requires WebGUI 7.8.1 or higher.
=item C<--parent=>
Specify the parent asset to import packages into.
=item C<-u> C<--upgrade>
If specified, packages will be exported to the directory for the upgrade to
the current local version.
=item C<--to=>
Specify a directory to output the package files to. If neither C<--upgrade>
or C<--to> is specified, packages will be output to the current directory.
=item C<< <asset> >>
Either an asset ID or an asset URL to specify an asset.
=back
=cut

1155
lib/WGDev/Command/Reset.pm Normal file

File diff suppressed because it is too large Load diff

40
lib/WGDev/Command/Run.pm Normal file
View file

@ -0,0 +1,40 @@
package WGDev::Command::Run;
# ABSTRACT: Run arbitrary shell command
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub process {
my $self = shift;
$self->wgd->set_environment;
my @arguments = $self->arguments;
my $command = shift @arguments;
my $result = system {$command} $command, @arguments;
return $result ? 0 : 1;
}
sub parse_params {
my ( $self, @args ) = @_;
$self->arguments( \@args );
return 1;
}
1;
=head1 SYNOPSIS
wgd run <command>
=head1 DESCRIPTION
Runs an arbitrary command, but sets the C<WEBGUI_CONFIG>, C<WEBGUI_ROOT>, and
C<PERL5LIB> environment variables first.
=head1 OPTIONS
Has no options of its own. All options are passed on to specified command.
=cut

View file

@ -0,0 +1,73 @@
package WGDev::Command::Self::Upgrade;
# ABSTRACT: Upgrade WGDev script
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::X;
use WGDev::Command;
sub needs_root { return }
sub config_options { () }
sub is_runnable {
# use the presence of fatpacker to detect single script install
# this command is not meant for upgrading module install
return $App::WGDev::PACKED;
}
sub process {
my $self = shift;
my $file = $0;
require File::Temp;
require LWP::UserAgent;
if (! -w $file) {
WGDev::X::IO::Write->throw( path => $file );
}
my $our_version = WGDev::Command->VERSION;
print "Current version: $our_version\n";
my $ua = LWP::UserAgent->new;
my $res = $ua->get('http://haarg.org/wgd');
if (! $res->is_success) {
WGDev::X->throw('Unable to download new version');
}
my $content = $res->decoded_content;
my $new_version = do {
my $temp_script = File::Temp->new;
$temp_script->autoflush(1);
print { $temp_script } $content;
open my $fh, q{-|}, $^X, q{--}, $temp_script->filename, '-V'
or WGDev::X::IO->throw;
my $output = do { local $/; <$fh> };
close $fh
or WGDev::X::IO->throw;
my ($script_version) = ($output =~ /(\d[\d.]+)/msx);
$script_version;
};
print "New version: $new_version\n";
if ($our_version eq $new_version) {
print "Already up to date.\n";
return 1;
}
print "Upgrading.\n";
open my $fh, '>', $file
or WGDev::X::IO->throw;
print { $fh } $content;
close $fh
or WGDev::X::IO->throw;
exec $^X, $file, '-V';
}
1;
=head1 SYNOPSIS
wgd self-upgrade
=head1 DESCRIPTION
Upgrades the WGDev script.
=cut

View file

@ -0,0 +1,75 @@
package WGDev::Command::Setting;
# ABSTRACT: Returns WebGUI settings from the database.
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
sub config_options {
return qw();
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my $dbh = $wgd->db->connect;
my $sth
= $dbh->prepare('SELECT name, value FROM settings WHERE name LIKE ?');
my $sth_set;
foreach my $argument ( $self->arguments ) {
my $new_value;
if ( $argument =~ s/=(.*)//msx ) {
$new_value = $1;
}
$sth->execute($argument);
while ( my ( $setting, $value ) = $sth->fetchrow_array ) {
if ( !defined $value ) {
$value = '(NULL)';
}
if ( defined $new_value ) {
$sth_set ||= $dbh->prepare(
'UPDATE settings SET value = ? WHERE name = ?');
$sth_set->execute( $new_value, $setting );
$sth_set->finish;
printf "%-39s %s => %s\n", $setting, $value, $new_value;
}
else {
printf "%-39s %s\n", $setting, $value;
}
}
}
return 1;
}
1;
=head1 SYNOPSIS
wgd setting <setting>[=<value>] [<setting> ...]
=head1 DESCRIPTION
Prints settings from the WebGUI settings table. This is handy for doing quick lookups,
or for using as part of other C<wgd> commands. Can also the the value of settings.
=head1 OPTIONS
=over 8
=item C<< <setting> >>
The name of the setting to display. Can also contain SQL wildcards
to show multiple settings. Using a setting of C<%> will display
all settings.
=item C<< <value> >>
The value to set the setting to. If specified, the old value and
new value will be included in the output.
=back
=cut

154
lib/WGDev/Command/Test.pm Normal file
View file

@ -0,0 +1,154 @@
package WGDev::Command::Test;
# ABSTRACT: Run WebGUI tests
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use File::Spec ();
sub config_parse_options { return qw(gnu_getopt pass_through) }
sub config_options {
return qw(
all|A
slow
live|L
debug
reset:s
cover|C:s
coverOptions:s
);
}
sub process {
my $self = shift;
require Cwd;
require App::Prove;
my $wgd = $self->wgd;
$wgd->set_environment;
if ( defined $self->option('reset') ) {
my $reset_options = $self->option('reset');
if ( $reset_options eq q{} ) {
$reset_options = '--quiet --backup --delcache --import --upgrade';
}
require WGDev::Command::Reset;
my $reset = WGDev::Command::Reset->new($wgd);
$reset->parse_params_string($reset_options);
$reset->process;
}
##no critic (RequireLocalizedPunctuationVars)
local $ENV{CODE_COP} = 1
if $self->option('slow');
local $ENV{TEST_SYNTAX} = 1
if $self->option('slow');
local $ENV{TEST_POD} = 1
if $self->option('slow');
local $ENV{WEBGUI_LIVE} = 1
if $self->option('live');
local $ENV{WEBGUI_TEST_DEBUG} = 1
if $self->option('debug');
local $ENV{HARNESS_PERL_SWITCHES} = $ENV{HARNESS_PERL_SWITCHES};
my $cover_dir;
if ( defined $self->option('cover') ) {
$cover_dir = $self->option('cover') || 'cover_db';
if ( -e $cover_dir ) {
system 'cover', '-silent', '-delete', $cover_dir;
}
my $cover_options = $self->option('coverOptions')
|| '-select,WebGUI,+ignore,^t';
if ( $ENV{HARNESS_PERL_SWITCHES} ) {
$ENV{HARNESS_PERL_SWITCHES} .= q{ };
}
else {
$ENV{HARNESS_PERL_SWITCHES} = q{};
}
$ENV{HARNESS_PERL_SWITCHES} .= '-MDevel::Cover=' . join q{,},
-silent => 1,
$cover_options, -db => $cover_dir;
}
my $prove = App::Prove->new;
my @args = $self->arguments;
@args = ( '-r', grep { $_ ne '-r' } @args );
my $orig_dir;
if ( $self->option('all') ) {
$orig_dir = Cwd::cwd();
chdir $wgd->root;
unshift @args, 't';
}
$prove->process_args(@args);
my $result = $prove->run;
if ($orig_dir) {
chdir $orig_dir;
}
if ( defined $cover_dir ) {
system 'cover', '-silent', $cover_dir;
}
return $result;
}
1;
=head1 SYNOPSIS
wgd test [-ASCL] [--debug] [<prove options>]
=head1 DESCRIPTION
Runs WebGUI tests, setting the needed environment variables beforehand.
Includes quick options for running all tests, and including slow tests.
=head1 OPTIONS
Unrecognized options will be passed through to prove.
=over 8
=item C<-A> C<--all>
Run all tests recursively. Otherwise, tests will need to be specified.
=item C<--slow>
Includes slow tests by defining CODE_COP, TEST_SYNTAX, and TEST_POD.
=item C<-L> C<--live>
Includes live tests by defining WEBGUI_LIVE.
=item C<--debug>
After a test, output the number of assets, version tags, users, groups, sessions
and session scratch variables, to determine when tests leak objects that can interfere
with downstream tests.
This option is really only useful when passing the --verbose switch through to prove.
=item C<--reset=>
Perform a site reset before running the tests. The value specified is used
as the command line parameters for the L<C<reset> command|WGDev::Command::Reset>.
With no value, will use the options C<--delcache --backup --import --upgrade> to do a
fast site reset.
=item C<-C> C<--cover=>
Run coverage using Devel::Cover. The value specified is used as the directory to
put the coverage data and defaults to C<cover_db>.
=item C<--coverOptions=>
Options to pass to L<Devel::Cover>. Defaults to C<-select,WebGUI,+ignore,^t>.
=back
=cut

110
lib/WGDev/Command/Trash.pm Normal file
View file

@ -0,0 +1,110 @@
package WGDev::Command::Trash;
# ABSTRACT: Trash assets by URL/assetId
use strict;
use warnings;
use WGDev::Command::Ls;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev ();
sub config_options {
return qw(
purge
restore
list
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
if ($self->option('list')) {
return $self->list_trash;
}
else {
return $self->trash;
}
}
sub trash {
my $self = shift;
my $wgd = $self->wgd;
my $error;
my $method = $self->option('purge') ? 'purge'
: $self->option('restore') ? 'restore'
: 'trash';
my @asset_specs = $self->arguments;
ASSET:
while ( my $asset_spec = shift @asset_specs ) {
my $asset;
if ( !eval { $asset = $wgd->asset->find($asset_spec) } ) {
warn "wgd trash: $asset_spec: No such asset\n";
$error++;
next ASSET;
}
my $success = $asset->$method;
if ($method ne 'restore' && ! $success) {
warn "wgd trash: unable to $method $asset_spec\n";
++$error;
}
}
return (! $error);
}
sub list_trash {
my $self = shift;
my $wgd = $self->wgd;
my $root = $wgd->asset->root;
my $trashed_assets = $root->getAssetsInTrash();
my $ls = WGDev::Command::Ls->new($wgd);
my $format = '%assetId% %url:-35% %title%';
ASSET:
foreach my $asset ( @{ $trashed_assets } ) {
next ASSET unless $asset;
my $output = $ls->format_output( $format, $asset );
print $output . "\n";
}
return 1;
}
1;
=head1 SYNOPSIS
wgd trash [--purge] [--restore] <asset> [<asset> ...]
wgd trash [--list]
=head1 DESCRIPTION
Methods for working with assets in the trash.
=head1 OPTIONS
=over 8
=item C<--purge>
Purges the assets from the system instead of putting it into the trash.
=item C<--restore>
Restores the assets that have been trashed to the regular, published state.
=item C<--list>
Lists all assets in the trash.
=item C<< <asset> >>
Either an asset URL or an ID. As many can be specified as desired.
Prepending with a slash will force it to be interpreted as a URL.
=back
=cut
1;

130
lib/WGDev/Command/User.pm Normal file
View file

@ -0,0 +1,130 @@
package WGDev::Command::User;
# ABSTRACT: Utilities for manipulating WebGUI Users
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::X ();
sub config_options {
return qw(
findByPassword=s
findByDictionary=s
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my $session = $wgd->session();
if ( my $password = $self->option('findByPassword') ) {
return $self->find_by_password($password);
}
if ( my $dictionary = $self->option('findByDictionary') ) {
return $self->find_by_dictionary($dictionary);
}
}
sub find_by_password {
my $self = shift;
my $password = shift;
my $session = $self->wgd->session();
require Digest::MD5;
require Encode;
my $hash = Digest::MD5::md5_base64( Encode::encode_utf8($password) );
print "Password:\t$password\n";
print "Hashes to:\t$hash\n";
my @user_ids = $session->db->buildArray(
'select userId from authentication where fieldName = ? and fieldData = ?',
[ 'identifier', $hash ] );
print "Matching users:\t";
print @user_ids ? "\n" : "None\n";
for my $user_id (@user_ids) {
my $user = WebGUI::User->new( $session, $user_id );
my $username = $user->username;
print " * $user_id ($username)\n";
}
return;
}
sub find_by_dictionary {
my $self = shift;
my $dict = shift;
my $session = $self->wgd->session();
my @hashed_passwords
= $session->db->buildArray(
'select fieldData from authentication where fieldName = ?',
['identifier'] );
my %hashed_passwords = map { $_ => 1 } @hashed_passwords;
open my $d, '<', $dict
or WGDev::X::IO::Read->throw(
path => $dict,
message => 'Unable to open dictionary file',
);
while ( my $word = <$d> ) {
chomp $word;
my $hash = Digest::MD5::md5_base64( Encode::encode_utf8($word) );
if ( $hashed_passwords{$hash} ) {
print "\n*** HIT ***\n";
$self->find_by_password($word);
}
}
close $d
or WGDev::X::IO::Read->throw(
path => $dict,
message => 'Unable to open dictionary file',
);
return;
}
1;
=head1 SYNOPSIS
wgd user [--findByPassword <password>] [--findByDictionary <dictionary>]
=head1 DESCRIPTION
Utilities for manipulating WebGUI Users
=head1 OPTIONS
=over 8
=item C<--findByPassword>
Return a list of users that are using the given password (assumes
WebGUI authentication module).
=item C<--findByDictionary>
Use a dictionary file to do a brute-force search for users using
any password in the dictionary (assumes WebGUI authentication
module). For example, Linux distributions typically have a dictionary
file in C</usr/share/dict/> or C</var/lib/dict/>
=back
=method find_by_password
Hashes the given password and sees if any user IDs in the C<authentication> table
match. This check will become less efficient once WebGUI implements password salting.
=method find_by_dictionary
Search through the given dictionary file, hashing words one by one and
checking them against all known hashed passwords.
Does not try to be efficient or clever - for common dictionary files it's
plenty fast enough.
=cut

84
lib/WGDev/Command/Util.pm Normal file
View file

@ -0,0 +1,84 @@
package WGDev::Command::Util;
# ABSTRACT: Run a utility script
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base::Verbosity);
use File::Spec ();
sub config_parse_options { return qw(gnu_getopt pass_through) }
sub process {
my $self = shift;
my $wgd = $self->wgd;
$wgd->set_environment;
my @args = $self->arguments;
my $command = shift @args;
unshift @args, '--configFile=' . $wgd->config_file_relative;
my $sbin_path = File::Spec->catdir( $wgd->root, 'sbin' );
if ( -e $command ) {
$command = File::Spec->rel2abs($command);
}
elsif ( -e File::Spec->rel2abs( $command, $sbin_path ) ) {
$command = File::Spec->rel2abs( $command, $sbin_path );
}
else {
WGDev::X->throw("Unable to find $command.");
}
if ( !-x $command ) {
unshift @args, $command;
# $^X is the name of the current perl executable
$command = $^X;
}
my $pid = fork;
if ( !$pid ) {
if ( $self->verbosity < 1 ) {
##no critic (RequireCheckedOpen)
open STDIN, '<', File::Spec->devnull;
open STDOUT, '>', File::Spec->devnull;
open STDERR, '>', File::Spec->devnull;
}
chdir $sbin_path;
exec {$command} $command, @args;
}
waitpid $pid, 0;
# $? is the child's exit value
return $? ? 0 : 1;
}
1;
=head1 SYNOPSIS
wgd util [-q] <command>
=head1 DESCRIPTION
Runs a utility script. The script will be run from WebGUI's F<sbin>
directory, and will be passed a C<--configFile> option.
=head1 OPTIONS
Any options not handled by this command are passed to the utility script.
=over 8
=item C<-q> C<--quiet>
If specified, will silence all output from the utility script.
=back
=cut

View file

@ -0,0 +1,217 @@
package WGDev::Command::Version;
# ABSTRACT: Reports and updates version numbers
use strict;
use warnings;
use 5.008008;
use parent qw(WGDev::Command::Base);
use WGDev::X ();
use File::Spec ();
sub needs_config {
return;
}
sub config_parse_options { return qw(no_getopt_compat) }
sub config_options {
return qw(
create|c
bare|b
dist|d
);
}
sub process {
my $self = shift;
my $wgd = $self->wgd;
my ($ver) = $self->arguments;
if ( $self->option('create') ) {
$ver = $self->update_version($ver);
}
my $wgv = $wgd->version;
my ( $perl_version, $perl_status ) = $wgv->module;
if ( $self->option('dist') ) {
print $perl_version, q{-}, $perl_status, "\n";
return 1;
}
if ( $self->option('bare') ) {
print $perl_version, "\n";
return 1;
}
my $db_version = $wgv->database_script;
my ( $change_file, $change_version ) = $wgv->changelog;
my ( $up_file, undef, $up_file_ver, $up_version ) = $wgv->upgrade;
my $db_live_version = eval { $wgv->database( $wgd->db->connect ) };
my $err_count = 0;
my $expect_ver = $ver || $perl_version;
if ( $perl_version ne $expect_ver ) {
$err_count++;
$perl_version = _colored( $perl_version, 'red' );
}
if ( $db_version ne $expect_ver ) {
$err_count++;
$db_version = _colored( $db_version, 'magenta' );
}
if ( $change_version ne $expect_ver ) {
$err_count++;
$change_version = _colored( $change_version, 'red' );
}
if ( $up_version ne $expect_ver ) {
$err_count++;
$up_version = _colored( $up_version, 'red' );
}
if ( $up_file_ver ne $expect_ver ) {
$err_count++;
$up_file = _colored( $up_file, 'red' );
}
if ( !defined $db_live_version ) {
$err_count++;
$db_live_version = _colored( 'Not available', 'magenta' );
}
elsif ( $db_live_version ne $expect_ver ) {
$err_count++;
$db_live_version = _colored( $db_live_version, 'red' );
}
print <<"END_REPORT";
Perl version: $perl_version - $perl_status
Database version: $db_live_version
Database script version: $db_version
Changelog version: $change_version
Upgrade script version: $up_version
Upgrade script filename: $up_file
END_REPORT
if ($err_count) {
return 0;
}
return 1;
}
sub update_version {
my $self = shift;
my $ver = shift;
my $wgd = $self->wgd;
my $root = $wgd->root;
my $wgv = $wgd->version;
my $old_version = $wgv->module;
my $new_version = $ver || do {
my @parts = split /[.]/msx, $old_version;
$parts[-1]++;
join q{.}, @parts;
};
open my $fh, '<', File::Spec->catfile( $root, 'lib', 'WebGUI.pm' )
or WGDev::X::IO::Read->throw( path => 'WebGUI.pm' );
my @pm_content = do { local $/; <$fh> };
close $fh
or WGDev::X::IO::Read->throw( path => 'WebGUI.pm' );
open $fh, '>', File::Spec->catfile( $root, 'lib', 'WebGUI.pm' )
or WGDev::X::IO::Write->throw( path => 'WebGUI.pm' );
for my $line (@pm_content) {
$line =~ s/(\$VERSION\s*=)[^\n]*;/$1 '$new_version';/msx;
print {$fh} $line;
}
close $fh
or WGDev::X::IO::Write->throw( path => 'WebGUI.pm' );
my ($change_file) = $wgv->changelog;
open $fh, '<',
File::Spec->catfile( $root, 'docs', 'changelog', $change_file )
or WGDev::X::IO::Read->throw( path => $change_file );
my $change_content = do { local $/; <$fh> };
close $fh
or WGDev::X::IO::Read->throw( path => $change_file );
open $fh, '>',
File::Spec->catfile( $root, 'docs', 'changelog', $change_file )
or WGDev::X::IO::Write->throw( path => $change_file );
print {$fh} $new_version . "\n\n" . $change_content;
close $fh
or WGDev::X::IO::Write->throw( path => $change_file );
open my $in, '<',
File::Spec->catfile( $root, 'docs', 'upgrades', '_upgrade.skeleton' )
or WGDev::X::IO::Read->throw( path => '_upgrade.skeleton' );
open my $out, '>',
File::Spec->catfile( $root, 'docs', 'upgrades',
"upgrade_$old_version-$new_version.pl" )
or WGDev::X::IO::Write->throw(
path => "upgrade_$old_version-$new_version.pl" );
while ( my $line = <$in> ) {
$line =~ s/(\$toVersion\s*=)[^\n]*$/$1 '$new_version';/xms;
print {$out} $line;
}
close $out
or WGDev::X::IO::Write->throw(
path => "upgrade_$old_version-$new_version.pl" );
close $in
or WGDev::X::IO::Read->throw( path => '_upgrade.skeleton' );
return $new_version;
}
sub _colored {
no warnings 'redefine';
if ( eval { require Term::ANSIColor; 1 } ) {
*_colored = \&Term::ANSIColor::colored;
}
else {
*_colored = sub { $_[0] };
}
goto &_colored;
}
1;
=head1 SYNOPSIS
wgd version [-b | -c | -d] [<version>]
=head1 DESCRIPTION
Reports the current versions of the F<WebGUI.pm> module, F<create.sql> database
script, change log, and upgrade file. Non-matching versions will be noted
in red if possible.
=head1 OPTIONS
=over 8
=item C<-c> C<--create>
Adds a new section to the change log for the new version, updates the version
number in F<WebGUI.pm>, and creates a new upgrade script. The version number
to update to can be specified on the command line. If not specified, defaults
to incrementing the patch level by one.
=item C<-d> C<--dist>
Output the version number and status of the current WebGUI, joined by a dash.
If the version is passed as well, it will be ignored.
=item C<-b> C<--bare>
Outputs the version number taken from F<WebGUI.pm> only
=item C<< <version> >>
The version number to compare against or create
=back
=method C<update_version ( $new_version )>
Updates WebGUI's version number to the specified version. If not provided,
the patch level of the version number is incremented. The version number in
F<WebGUI.pm> is changed, a new upgrade script is created, and a heading is
added to the change log.
=cut

223
lib/WGDev/Database.pm Normal file
View file

@ -0,0 +1,223 @@
package WGDev::Database;
# ABSTRACT: Database connectivity and DSN parsing for WGDev
use strict;
use warnings;
use 5.008008;
use WGDev::X ();
sub username { return shift->{username} }
sub password { return shift->{password} }
sub database { return shift->{database} }
sub hostname { return shift->{hostname} }
sub port { return shift->{port} }
sub dsn { return shift->{dsn} }
sub user { goto &username }
sub pass { goto &password }
sub host { goto &hostname }
sub name { goto &database }
sub new {
my $class = shift;
my $config = shift;
my $self = bless {}, $class;
my $dsn = $self->{dsn} = $config->get('dsn');
$self->{username} = $config->get('dbuser');
$self->{password} = $config->get('dbpass');
$self->{database} = ( split /[:;]/msx, $dsn )[2];
$self->{hostname} = 'localhost';
$self->{port} = '3306';
while ( $dsn =~ /([^=;:]+)=([^;:]+)/msxg ) {
if ( $1 eq 'host' || $1 eq 'hostname' ) {
$self->{hostname} = $2;
}
elsif ( $1 eq 'db' || $1 eq 'database' || $1 eq 'dbname' ) {
$self->{database} = $2;
}
elsif ( $1 eq 'port' ) {
$self->{port} = $2;
}
}
return $self;
}
sub command_line {
my $self = shift;
my @params = (
'-h' . $self->hostname,
'-P' . $self->port,
'-u' . $self->username,
( $self->password ? '-p' . $self->password : () ),
'--default-character-set=utf8',
@_,
$self->database,
);
return wantarray ? @params : join q{ }, map {"'$_'"} @params;
}
sub connect { ## no critic (ProhibitBuiltinHomonyms)
my $self = shift;
require DBI;
if ( $self->{dbh} && !$self->{dbh}->ping ) {
delete $self->{dbh};
}
return $self->{dbh} ||= DBI->connect(
$self->dsn,
$self->username,
$self->password,
{
RaiseError => 1,
PrintWarn => 0,
PrintError => 0,
mysql_enable_utf8 => 1,
} );
}
sub dbh { return shift->{dbh} }
sub open { goto &connect } ## no critic (ProhibitBuiltinHomonyms)
sub disconnect {
my $self = shift;
if ( my $dbh = delete $self->{dbh} ) {
$dbh->disconnect;
}
return;
}
sub close { ## no critic (ProhibitBuiltinHomonyms ProhibitAmbiguousNames)
goto &disconnect;
}
sub clear {
my $self = shift;
my $dbh = $self->connect;
my $sth = $dbh->table_info( undef, undef, q{%} );
my @tables = map { @{$_} } @{ $sth->fetchall_arrayref( [2] ) };
$dbh->do('SET FOREIGN_KEY_CHECKS = 0');
for my $table (@tables) {
$dbh->do( 'DROP TABLE ' . $dbh->quote_identifier($table) );
}
$dbh->do('SET FOREIGN_KEY_CHECKS = 1');
return 1;
}
sub load {
my $self = shift;
my $dumpfile = shift;
system 'mysql', $self->command_line( '-e' . 'source ' . $dumpfile )
and WGDev::X::System->throw('Error running mysql');
return 1;
}
sub dump { ## no critic (ProhibitBuiltinHomonyms)
my $self = shift;
my $dumpfile = shift;
system 'mysqldump', $self->command_line( '-r' . $dumpfile )
and WGDev::X::System->throw('Error running mysqldump');
return 1;
}
1;
=head1 SYNOPSIS
my $dsn = $wgd->database->connect;
my $username = $wgd->database->username;
=head1 DESCRIPTION
Has methods to access various parts of the DSN that can be used for other
programs such as command line C<mysql>. Also has methods to easily connect
and reuse a database connection.
=method C<new ( $wgd )>
Creates a new WGDev::Database object.
=for :list
= C<$wgd>
An instantiated WGDev object.
=method C<dsn>
Returns the DSN for the database.
=method C<database>
Returns the name of the database.
=method C<name>
Alias for the L</database> method.
=method C<hostname>
Returns the host name for the database connection.
=method C<host>
Alias for the L</hostname> method.
=method C<password>
Returns the password for the database connection.
=method C<pass>
Alias for the L</password> method.
=method C<port>
Returns the port for the database connection.
=method C<username>
Returns the user name for the database connection.
=method C<user>
Alias for the L</username> method.
=method C<command_line>
Returns command line options suitable for passing to the F<mysql>
or F<mysqldump> command line programs to connect to the database.
=method C<connect>
Connects to the database if it hasn't been connected to yet and
returns the database handle for the connection.
=method C<open>
Alias for the L</connect> method.
=method C<dbh>
Returns the database handle of the current connection, or C<undef> if
there is no active connection.
=method C<disconnect>
Closes the active database connection. If there is no active
connection, does nothing.
=method C<close>
Alias for the L</disconnect> method.
=method C<clear>
Removes all tables from the database, leaving it empty.
=method C<dump ( $dumpfile )>
Dumps the database content to the specified file.
=method C<load ( $dumpfile )>
Loads the specified database script into the database.
=cut

98
lib/WGDev/File.pm Normal file
View file

@ -0,0 +1,98 @@
package WGDev::File;
# ABSTRACT: File utility functions
use strict;
use warnings;
use 5.008008;
use constant STAT_FILESIZE => 7;
use WGDev::X;
use File::Spec ();
sub sync_dirs {
my $class = shift;
my $from_dir = shift;
my $to_dir = shift;
require File::Copy;
require File::Path;
File::Path::mkpath($to_dir);
# recurse through destination and delete files that don't exist in source
$class->matched_find($to_dir, $from_dir, sub {
my ($to_path, $from_path) = @_;
return
if -e $from_path;
if ( -d $to_path ) {
File::Path::rmtree($to_path);
}
else {
unlink $to_path;
}
});
# copy files that don't exist or are different
$class->matched_find($from_dir, $to_dir, sub {
my ($from_path, $to_path) = @_;
return
if -d $from_path;
my $from_size = ( stat _ )[STAT_FILESIZE];
return
if -e $to_path && ( stat _ )[STAT_FILESIZE] == $from_size;
my $to_parent = File::Spec->catpath(
( File::Spec->splitpath($to_path) )[ 0, 1 ] );
File::Path::mkpath($to_parent);
File::Copy::copy( $from_path, $to_path );
});
}
sub matched_find {
my $class = shift;
my $from_dir = shift;
my $to_dir = shift;
my $cb = shift;
require File::Find;
my $matched_cb = sub {
no warnings 'once';
my $from_path = $File::Find::name;
my ( undef, undef, $filename ) = File::Spec->splitpath($from_path);
if ( $filename eq '.svn' || $filename eq 'temp' ) {
$File::Find::prune = 1;
return;
}
my $rel_path = File::Spec->abs2rel( $from_path, $from_dir );
my $to_path = File::Spec->rel2abs( $rel_path, $to_dir );
$cb->($from_path, $to_path);
};
File::Find::find( { no_chdir => 1, wanted => $matched_cb }, $from_dir );
}
1;
=head1 SYNOPSIS
WGDev::File->sync_dirs($from, $to);
=head1 DESCRIPTION
Performs common actions on files.
=method C<sync_dirs ( $from_dir, $to_dir )>
Synchronizes two directories. Deletes any additional files in the
destination that don't exist in the source. Checks for file
differences by size before copying.
=method C<matched_find ( $from_dir, $to_dir, $callback )>
Recurses through C<$from_dir>, calling C<$callback> for each file
or directory found. The callback is passed two parameters, the
file found, and a file name relative to C<$to_dir> based on the found
file's path relative to C<$from_dir>.
=cut

213
lib/WGDev/Help.pm Normal file
View file

@ -0,0 +1,213 @@
package WGDev::Help;
# ABSTRACT: Generate help text for WGDev
use strict;
use warnings;
use 5.008008;
use WGDev::X ();
use File::Spec ();
use Try::Tiny;
sub package_usage {
my $package = shift;
my $verbosity = shift;
require WGDev::Pod::Usage;
if ( !defined $verbosity ) {
$verbosity = 1;
}
my $parser = WGDev::Pod::Usage->new;
$parser->verbosity($verbosity);
my $pod = package_pod($package);
return $parser->parse_from_string($pod);
}
sub package_perldoc {
my $package = shift;
my $sections = shift;
require Pod::Perldoc;
require File::Temp;
File::Temp->VERSION(0.19); ##no critic (ProhibitMagicNumbers)
require File::Path;
my $pod = package_pod( $package, $sections );
my $pid = fork;
if ( !$pid ) {
# perldoc will try to drop privs anyway, so do it ourselves so the
# temp file has the correct owner
Pod::Perldoc->new->drop_privs_maybe;
# Make a path that plays nice with Perldoc internals. Will format nicer.
my $tmpdir = File::Temp->newdir( TMPDIR => 1 );
# construct a path that Perldoc will interperet as a package name
my @path_parts = split /::/msx, $package;
my $filename = pop @path_parts;
my $path = File::Spec->catdir( $tmpdir->dirname, 'perl', @path_parts );
File::Path::mkpath($path);
my $out_file = File::Spec->catfile( $path, $filename );
open my $out, '>', $out_file
or WGDev::X::IO->throw('Unable to create temp file');
print {$out} $pod;
close $out or return q{};
# perldoc doesn't understand darwin's stty output.
# copy and paste but it seems to work
my @extra_args;
if ($^O eq 'darwin') {
##no critic (ProhibitBacktick ProhibitMagicNumbers)
if (`stty -a` =~ /(\d+)[ ]columns;/msx) {
my $cols = $1;
my $c = $cols * 39 / 40;
$cols = $c > $cols - 2 ? $c : $cols -2;
if ( $cols > 80 ) {
push @extra_args, '-n', 'nroff -rLL=' . (int $c) . 'n';
}
}
}
local @ARGV = ( @extra_args, '-w', 'section:3', '-F', $out_file );
exit Pod::Perldoc->run;
}
waitpid $pid, 0;
# error status of subprocess
if ($?) {
WGDev::X->throw('Error displaying help!');
}
return;
}
my %pod;
sub package_pod {
my $package = shift;
my $sections = shift;
my $raw_pod = $pod{$package};
if ( !$raw_pod ) {
$raw_pod = read_lib($package);
$pod{$package} = $raw_pod;
}
return $raw_pod
if !$sections;
open my $pod_in, '<', \$raw_pod
or WGDev::X::IO->throw;
my @sections = ref $sections ? @{$sections} : $sections;
require Pod::Select;
my $parser = Pod::Select->new;
$parser->select(@sections);
my $pod = q{};
open my $pod_out, '>', \$pod
or WGDev::X::IO->throw;
$parser->parse_from_filehandle( $pod_in, $pod_out );
close $pod_out
or WGDev::X::IO->throw;
close $pod_in
or WGDev::X::IO->throw;
return $pod;
}
sub read_lib {
my $module = shift;
if ($module =~ /\A\w+(?:::\w+)*\z/msx) {
$module .= '.pm';
$module =~ s{::}{/}msxg;
}
my $data;
if ($INC{$module}) {
$data = _read_file($module, $INC{$module});
}
else {
for my $inc (@INC) {
if (! ref $inc) {
my $filename = $inc . q{/} . $module;
if (-f $filename) {
$data = _read_file($module, $filename);
}
}
else {
$data = _read_file($module, $inc);
}
last
if defined $data;
}
}
return $data;
}
sub _read_file {
my ($module, $inc) = @_;
my ($fh, $cb, $state);
##no critic (ProhibitCascadingIfElse)
if (! ref $inc) {
open $fh, '<', $inc
or return;
}
elsif (ref $inc eq 'CODE') {
($fh, $cb, $state) = $inc->($inc, $module);
}
elsif (ref $inc eq 'ARRAY') {
($fh, $cb, $state) = $inc->[0]->($inc, $module);
}
elsif ($inc->can('INC')) {
($fh, $cb, $state) = $inc->INC($module);
}
my $data;
if ($cb || $fh) {
local $_;
$data = q{};
while (1) {
last
if ($fh && !defined ($_ = <$fh>));
last
if ($cb && !$cb->($cb, $state));
$data .= $_;
}
if ($fh) {
close $fh
or WGDev::X::IO->throw;
}
}
return $data;
}
1;
=head1 SYNOPSIS
use WGDev::Help;
my $usage = WGDev::Help::package_usage( 'My::Class' );
=head1 DESCRIPTION
Reads help information from modules but filters to only pick relevant
sections when multiple POD documents exist in a single file.
=func C<package_usage ( $package [, $verbosity] )>
Returns usage information for a package, using L<Pod::Usage>. Can be used on
packages that have been combined into a single file.
=func C<package_perldoc ( $package [, $sections] )>
Displays documentation for a package using L<Pod::Perldoc>. Can be used on
packages that have been combined into a single file.
=for :list
= C<$sections>
Passed on to L</package_pod> to limit the sections output.
=func C<package_pod ( $package [, $sections] )>
Filters out the POD for a specific package from the module file for the package.
=for :list
= C<$sections>
Limits sections to include based on L<Pod::Select/SECTION SPECIFICATIONS|Pod::Select's rules>.
Can be either a scalar value or an array reference.
=cut

162
lib/WGDev/Pod/Usage.pm Normal file
View file

@ -0,0 +1,162 @@
package WGDev::Pod::Usage;
# ABSTRACT: Produce usage documentation for WGDev commands
use strict;
use warnings;
use 5.008008;
use constant OPTION_INDENT => 4;
use constant OPTION_TEXT_INDENT => 24;
use parent qw(Pod::PlainText Pod::Select);
use WGDev::X ();
sub new {
my $proto = shift;
my $self = $proto->SUPER::new( indent => 0 );
$self->verbosity(1);
return $self;
}
sub verbosity {
my $self = shift;
my $verbosity = shift;
if ($verbosity) {
$self->select(qw(NAME SYNOPSIS OPTIONS/!.+));
}
else {
$self->select(qw(NAME SYNOPSIS));
}
return;
}
sub command {
my $self = shift;
my $command = shift;
$self->{_last_command} = $command;
return $self->SUPER::command( $command, @_ );
}
sub cmd_head1 {
my $self = shift;
my $head = shift;
my $para = shift;
$head =~ s/\s+$//msx;
$self->{_last_head1} = $head;
if ( $head eq 'NAME' ) {
return;
}
elsif ( $head eq 'SYNOPSIS' ) {
$head = 'USAGE';
}
$head = lc $head;
$head =~ s/\b(.)/uc($1)/msxe;
$head .= q{:};
my $output = $self->interpolate( $head, $para );
$self->output( $output . "\n" );
return;
}
sub textblock {
my $self = shift;
my $text = shift;
my $para = shift;
if ( $self->{_last_head1} eq 'NAME' ) {
$text =~ s/^[\w:]+\Q - //msx;
}
if ( $self->{_last_command} eq 'item' && !$self->{ITEM} ) {
return;
}
return $self->SUPER::textblock( $text, $para );
}
sub verbatim {
my $self = shift;
if ( $self->{_last_command} eq 'item' && !$self->{ITEM} ) {
return;
}
return $self->SUPER::verbatim(@_);
}
sub item {
my $self = shift;
my $item = shift;
my $tag = delete $self->{ITEM};
my $margin = $self->{MARGIN};
local $self->{MARGIN} = 0; ## no critic (ProhibitLocalVars)
$tag = $self->reformat($tag);
$tag =~ s/\n*\z//msx;
$item =~ s/[.].*//msx;
{
## no critic (ProhibitLocalVars)
local $self->{width} = $self->{width} - OPTION_TEXT_INDENT;
$item = $self->reformat($item);
}
$item =~ s/\n*\z//msx;
my $option_indent_string = q{ } x OPTION_TEXT_INDENT;
$item =~ s/\n/\n$option_indent_string/msxg;
my $indent_string = q{ } x OPTION_INDENT;
if ( $item eq q{} ) {
$self->output( $indent_string . $tag . "\n" );
}
else {
my $option_name_length = OPTION_TEXT_INDENT - OPTION_INDENT - 1;
$self->output( $indent_string . sprintf "%-*s %s\n",
$option_name_length, $tag, $item );
}
return;
}
sub seq_c {
return $_[1];
}
sub parse_from_string {
my $self = shift;
my $pod = shift;
my $output = q{};
open my $out_fh, '>', \$output
or WGDev::X::IO->throw;
open my $in_fh, '<', \$pod
or WGDev::X::IO->throw;
$self->parse_from_filehandle( $in_fh, $out_fh );
close $in_fh
or WGDev::X::IO->throw;
close $out_fh
or WGDev::X::IO->throw;
return $output;
}
# this/these are methods that Pod::PlainText used to implement, but no long does
sub cmd_method { my $self = shift; $self->item(@_); } # 'cmd_' . $pod_node_name is a computed method name; handle =method entires the same way as =item entries
# done
1;
=head1 SYNOPSIS
use WGDev::Pod::Usage;
my $parser = WGDev::Pod::Usage->new;
my $usage = $parser->parse_from_string($pod);
=head1 DESCRIPTION
Formats POD documentation into a format suitable for showing as
usage text. WGDev::Pod::Usage is a subclass of L<Pod::Select>.
=for Pod::Coverage
cmd_head1
new
parse_from_string
seq_c
textblock
verbosity
=cut

216
lib/WGDev/Version.pm Normal file
View file

@ -0,0 +1,216 @@
package WGDev::Version;
# ABSTRACT: Extract version information from WebGUI
use strict;
use warnings;
use 5.008008;
use File::Spec;
use WGDev::X ();
sub new {
my $class = shift;
my $dir = shift || WGDev::X::NoWebGUIRoot->throw;
my $self = bless \$dir, $class;
return $self;
}
sub pm { goto &module }
sub module {
my $dir = ${ +shift };
my $version;
my $status;
open my $fh, '<', File::Spec->catfile( $dir, 'lib', 'WebGUI.pm' )
or WGDev::X::IO::Read->throw( path => 'WebGUI.pm' );
while ( my $line = <$fh> ) {
##no critic (ProhibitStringyEval)
if ( $line =~ /\$VERSION\s*=(.*)$/msx ) {
$version = eval $1;
}
if ( $line =~ /\$STATUS\s*=(.*)$/msx ) {
$status = eval $1;
}
last
if $version && $status;
}
close $fh
or WGDev::X::IO::Read->throw( path => 'WebGUI.pm' );
return wantarray ? ( $version, $status ) : $version;
}
sub db_script { goto &database_script }
sub database_script {
my $self = shift;
my $dir = ${ $self };
my $wg8 = $self->module =~ /^8[.]/msx;
my $version;
my $db_file = $wg8 ? do {
require WebGUI::Paths;
WebGUI::Paths->defaultCreateSQL;
} : File::Spec->catfile( $dir, 'docs', 'create.sql' );
open my $fh, '<', $db_file
or WGDev::X::IO::Read->throw( path => $db_file );
while ( my $line = <$fh> ) {
if (
##no critic (ProhibitComplexRegexes);
$line =~ m{
(?:(?i)\QINSERT INTO\E) \s+
(`?)webguiVersion\1 \s+
.+? \s+?
(?i)VALUES \s+
\Q('\E ( [^']+ ) [']
}msx
) {
$version = $2;
last;
}
}
close $fh
or WGDev::X::IO::Read->throw( path => $db_file );
return $version;
}
sub db { goto &database }
sub database {
my $dir = ${ +shift };
my $dbh = shift;
require version;
my $sth = $dbh->prepare('SELECT webguiVersion FROM webguiVersion');
$sth->execute;
my @versions = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, version->new($_) ] }
map { @{$_} } @{ $sth->fetchall_arrayref( [0] ) };
$sth->finish;
my $version = pop @versions;
return $version;
}
sub changelog {
my $dir = ${ +shift };
require version;
my @changelogs;
opendir my $dh, File::Spec->catdir( $dir, 'docs', 'changelog' )
or WGDev::X::IO::Read->throw( path => 'docs/changelog' );
while ( my $file = readdir $dh ) {
if ( $file =~ /^( [x\d]+ [.] [x\d]+ [.] [x\d]+ ) \Q.txt\E $/msx ) {
( my $v = $1 ) =~ tr/x/0/;
push @changelogs, [ $file, version->new($v) ];
}
}
closedir $dh
or WGDev::X::IO::Read->throw( path => 'docs/changelog' );
@changelogs = sort { $a->[1] <=> $b->[1] } @changelogs;
my $latest = pop @changelogs;
open my $fh, '<',
File::Spec->catfile( $dir, 'docs', 'changelog', $latest->[0] )
or WGDev::X::IO::Read->throw( path => "docs/changelog/$latest->[0]" );
while ( my $line = <$fh> ) {
if ( $line =~ /^(\d+[.]\d+[.]\d+)$/msx ) {
$latest->[1] = $1;
last;
}
}
close $fh
or WGDev::X::IO::Read->throw( path => "docs/changelog/$latest->[0]" );
return @{$latest};
}
# returns ($upgrade_file, $from_version, $to_version, $to_version_file)
sub upgrade {
my $dir = ${ +shift };
require version;
my @upgrades;
opendir my $dh, File::Spec->catdir( $dir, 'docs', 'upgrades' )
or WGDev::X::IO::Read->throw( path => 'docs/upgrades' );
while ( my $file = readdir $dh ) {
if ( $file =~ /^upgrade_ ([.\d]+) - ([.\d]+) \Q.pl\E$/msx ) {
push @upgrades, [ $file, version->new($1), version->new($2) ];
}
}
closedir $dh
or WGDev::X::IO::Read->throw( path => 'docs/upgrades' );
@upgrades = sort { $a->[2] <=> $b->[2] } @upgrades;
my $latest = pop @upgrades;
open my $fh, '<',
File::Spec->catfile( $dir, 'docs', 'upgrades', $latest->[0] )
or
WGDev::X::IO::Read->throw( path => 'docs/upgrades/' . $latest->[0] );
while ( my $line = <$fh> ) {
if ( $line =~ /\$toVersion\s*=(.*)$/msx ) {
##no critic (ProhibitStringyEval RequireCheckingReturnValueOfEval)
push @{$latest}, eval $1;
last;
}
}
close $fh
or
WGDev::X::IO::Read->throw( path => 'docs/upgrades/' . $latest->[0] );
return @{$latest};
}
1;
=head1 SYNOPSIS
my $wgv = WGDev::Version->new('/data/WebGUI');
print "You have WebGUI " . $wgv->module . "\n";
=head1 DESCRIPTION
Extracts version information from various places in WebGUI: the change log,
the upgrade script, the WebGUI module, the database creation script, or a
live database.
=method C<new ( $webgui_root )>
Creates a new WGDev::Version object. Needs a WebGUI directory to be specified.
=for :list
= C<$webgui_root>
The root of the WebGUI directory to use for finding each file.
=method C<module>
In scalar context, returns the version number from the F<lib/WebGUI.pm>
module. In array context, returns the version number and the status
(beta/stable).
=method C<pm>
An alias for the L</module> method.
=method C<changelog>
Returns the most recent version number noted in the change log.
=method C<upgrade>
my ($upgrade_file, $from_version, $to_version, $to_version_file) = $wgv->upgrade;
Finds the most recent upgrade script and returns an array of
information about it. The array contains the script's file name,
the version number it will upgrade from and to based on its file name,
and the version it will upgrade to noted in the script itself.
=method C<database_script>
Returns the version noted in the F<create.sql> database script.
=method C<db_script>
An alias for the L</database_script> method.
=method C<database ( $dbh )>
Accepts a database handle, and returns the latest version from the
C<webguiVersion> table.
=method C<db>
An alias for the L</database> method.
=cut

263
lib/WGDev/X.pm Normal file
View file

@ -0,0 +1,263 @@
package WGDev::X;
# ABSTRACT: WGDev Exceptions
use strict;
use warnings;
use 5.008008;
use Exception::Class (
'WGDev::X' => { description => 'A general WGDev error', },
'WGDev::X::CommandLine' => {
isa => 'WGDev::X',
description => 'An error with the command line.',
fields => ['usage'],
},
'WGDev::X::CommandLine::BadCommand' => {
isa => 'WGDev::X::CommandLine',
description => 'An invalid command was requested.',
fields => ['command_name'],
},
'WGDev::X::BadCommand' => {
isa => 'WGDev::X',
description => 'An invalid command was requested.',
fields => ['command_name'],
},
'WGDev::X::CommandLine::BadParams' => {
isa => 'WGDev::X::CommandLine',
description => 'Invalid parameters were passed to a command.',
},
'WGDev::X::System' => {
isa => 'WGDev::X',
description => 'System error',
fields => ['errno_string'],
},
'WGDev::X::IO' => {
isa => 'WGDev::X::System',
description => 'IO error',
fields => ['path'],
},
'WGDev::X::IO::Read' => {
isa => 'WGDev::X::IO',
description => 'Read error',
},
'WGDev::X::IO::Write' => {
isa => 'WGDev::X::IO',
description => 'Write error',
},
'WGDev::X::NoWebGUIConfig' => {
isa => 'WGDev::X',
description => 'No WebGUI config file available.',
},
'WGDev::X::NoWebGUIRoot' => {
isa => 'WGDev::X',
description => 'No WebGUI root directory available.',
},
'WGDev::X::BadParameter' => {
isa => 'WGDev::X',
description => 'Bad parameter provided.',
fields => [ 'parameter', 'value' ],
},
'WGDev::X::AssetNotFound' => {
isa => 'WGDev::X',
description => 'Specified asset not found',
fields => ['asset'],
},
'WGDev::X::BadAssetClass' => {
isa => 'WGDev::X',
description => 'Bad asset class specified',
fields => ['class'],
},
'WGDev::X::Module' => {
isa => 'WGDev::X',
description => 'Error loading module',
fields => ['module', 'using_module'],
},
'WGDev::X::Module::Find' => {
isa => 'WGDev::X::Module',
description => q{Can't find module},
},
'WGDev::X::Module::Parse' => {
isa => 'WGDev::X::Module',
description => q{Error compiling module},
},
'WGDev::X::BadPackage' => {
isa => 'WGDev::X',
description => q{Error importing a package},
fields => ['message', 'package'],
},
);
BEGIN {
if ( $ENV{WGDEV_DEBUG} ) {
WGDev::X->Trace(1);
}
##no critic (ProhibitMagicNumbers)
if ( !eval { Exception::Class->VERSION(1.27) } ) {
# work around bad behavior of Exception::Class < 1.27
# where it defaults the message to $!
no warnings 'once';
*WGDev::X::new = sub {
my $errno = qq{$!};
my $class = shift;
my $self = $class->SUPER::new(@_);
if ( $self->{message} eq $errno ) {
$self->{message} = q{};
}
return $self;
};
}
}
##no critic (ProhibitQualifiedSubDeclarations)
sub _format_file_as_module {
my $file = shift;
if ($file =~ s/[.]pm$//msx) {
$file =~ s{/}{::}msxg;
}
return $file;
}
sub WGDev::X::inflate {
my $class = shift;
if (@_ == 1 && ref $_[0] && $_[0]->can('throw')) {
$_[0]->throw;
}
if (@_ == 1 && !ref $_[0]) {
my $e = shift;
##no critic (ProhibitComplexRegexes);
if ($e =~ m{
\ACan't[ ]locate[ ](.*?)[ ]in[ ][@]INC[ ]
.*[ ]at[ ](.*?)[ ]line[ ]\d+[.]
}msx) {
my $module = $1;
my $using_module = $2;
$module = _format_file_as_module($module);
$using_module = _format_file_as_module($using_module);
WGDev::X::Module::Find->throw(message => $e, module => $module, using_module => $using_module);
}
elsif ( $e =~ s{
(at[ ](.*?)[.]pm[ ]line[ ]\d+[.])
\s+Compilation[ ]failed[ ]in[ ]require[ ]at[ ]
(.*?)[ ]line[ ]\d+[.].*?\z
}{$1}msx ) {
my $module = $2;
my $using_module = $3;
$module = _format_file_as_module($module);
$using_module = _format_file_as_module($using_module);
WGDev::X::Module::Parse->throw(message => $e, module => $module, using_module => $using_module);
}
}
$class->throw(@_);
}
sub WGDev::X::full_message {
my $self = shift;
return $self->message || $self->description;
}
sub WGDev::X::CommandLine::full_message {
my $self = shift;
my $message = $self->message;
if ( defined $self->usage ) {
if ($message) {
$message =~ s/[\n\r]*\z/\n\n/msx;
}
$message .= $self->usage;
}
$message =~ s/[\n\r]*\z/\n\n/msx;
return $message;
}
sub WGDev::X::BadParameter::full_message {
my $self = shift;
my $message = $self->SUPER::message || $self->description;
if ( $self->parameter ) {
$message .= q{ } . $self->parameter;
}
if ( $self->value ) {
$message .= q{: } . $self->value;
}
return $message;
}
sub WGDev::X::CommandLine::BadCommand::full_message {
my $self = shift;
my $message
= defined $self->command_name
? "Can't find command @{[ $self->command_name ]}!\n"
: "No command specified!\n";
if ( defined $self->usage ) {
$message .= "\n" . $self->usage;
}
$message =~ s/[\n\r]*\z/\n\n/msx;
$message
.= "Try the running 'wgd commands' for a list of available commands.\n\n";
return $message;
}
sub WGDev::X::System::new {
my $class = shift;
my $errno_string = qq{$!};
my $self = $class->SUPER::new(@_);
if ( !defined $self->errno_string ) {
$self->{errno_string} = $errno_string;
}
return $self;
}
sub WGDev::X::System::full_message {
my $self = shift;
my $message = $self->SUPER::full_message;
$message .= ' - ' . $self->errno_string;
return $message;
}
sub WGDev::X::IO::full_message {
my $self = shift;
my $message = $self->SUPER::message || $self->description;
if ( $self->path ) {
$message .= ' Path: ' . $self->path;
}
$message .= ' - ' . $self->errno_string;
return $message;
}
sub WGDev::X::AssetNotFound::full_message {
my $self = shift;
my $message = $self->SUPER::full_message;
if ( $self->asset ) {
$message .= ' - ' . $self->asset;
}
return $message;
}
sub WGDev::X::Module::full_message {
my $self = shift;
my $message = $self->description . q{ } . $self->module
. q{ for } . $self->using_module . ":\n" . $self->SUPER::message;
$message =~ s/[\n\r]*\z/\n\n/msx;
return $message;
}
sub WGDev::X::Module::Find::full_message {
my $self = shift;
my $message = $self->description . q{ } . $self->module
. q{ for } . $self->using_module;
$message =~ s/[\n\r]*\z/\n\n/msx;
return $message;
}
1;
=head1 SYNOPSIS
use WGDev::X;
WGDev::X->throw();
=head1 DESCRIPTION
Exceptions for WGDev
=cut

View file

@ -240,7 +240,7 @@ sub getEditForm {
my $i18n = WebGUI::International->new($self->session,"Asset_Poll");
my ($i, $answers);
for ($i=1; $i<=20; $i++) {
if ($self->get('a'.$i) =~ /\C/) {
if ($self->get('a'.$i) =~ /./) {
$answers .= $self->getValue("a".$i)."\n";
}
}
@ -489,7 +489,7 @@ sub view {
$var{"form.end"} = WebGUI::Form::formFooter($self->session,);
$totalResponses = 1 if ($totalResponses < 1);
for (my $i=1; $i<=20; $i++) {
if ($self->get('a'.$i) =~ /\C/) {
if ($self->get('a'.$i) =~ /./) {
my ($tally) = $self->session->db->quickArray("select count(*) from Poll_answer where answer='a"
.$i."' and assetId=".$self->session->db->quote($self->getId)." group by answer");
push(@answers,{

246
lib/WebGUI/Paths.pm Normal file
View file

@ -0,0 +1,246 @@
package WebGUI::Paths;
=head1 LEGAL
-------------------------------------------------------------------
WebGUI is Copyright 2001-2012 Plain Black Corporation.
-------------------------------------------------------------------
Please read the legal notices (docs/legal.txt) and the license
(docs/license.txt) that came with this distribution before using
this software.
-------------------------------------------------------------------
http://www.plainblack.com info@plainblack.com
-------------------------------------------------------------------
=cut
our $VERSION = '0.0.1';
use 5.010;
use strict;
use warnings;
use Carp qw(croak);
use Cwd qw(realpath);
use File::Spec::Functions qw(catdir splitpath catpath splitpath updir catfile);
use Try::Tiny;
use namespace::clean;
=head1 NAME
Package WebGUI::Paths
=head1 DESCRIPTION
Locations for WebGUI files
=head1 IMPORT OPTIONS
=head2 -inc
use WebGUI::Paths -inc;
Loads all of the entries from the preload.custom file into @INC
=head2 -preload
Loads all of the entries from the preload.custom file into @INC,
and loads all modules in the WebGUI namespace.
=head1 METHODS
These methods are available from this class:
=head2 configBase
Returns the base directory for WebGUI site config files.
=head2 logConfig
Returns the file path of the log configuration file.
=head2 spectreConfig
Returns the file path of the Spectre configuration file.
=head2 preloadCustom
Returns the file path of the preload.custom file to use.
=head2 preloadExclusions
Returns the file path of the preload.exclude file to use.
=head2 upgrades
Returns the base directory that contains the upgrade scripts.
=head2 extras
Returns the base directory of the WebGUI extra web files.
=head2 defaultUploads
Returns the base directory of the default site uploads content.
=head2 defaultCreateSQL
Returns the file path of the default site create.sql script.
=head2 share
Returns the base directory for WebGUI auxiliary files.
=cut
BEGIN {
my $root = realpath(catdir(
catpath((splitpath(__FILE__))[0,1], ''), (updir) x 2
));
my %paths = (
configBase => catdir($root, 'etc'),
logConfig => catfile($root, 'etc', 'log.conf'),
spectreConfig => catfile($root, 'etc', 'spectre.conf'),
preloadCustom => catfile($root, 'etc', 'preload.custom'),
preloadExclusions => catfile($root, 'etc', 'preload.exclude'),
upgrades => catdir($root, 'share', 'upgrades'),
extras => catdir($root, 'www', 'extras'),
defaultUploads => catdir($root, 'www', 'uploads'),
defaultCreateSQL => catdir($root, 'share', 'create.sql'),
share => catdir($root, 'share'),
defaultPSGI => catdir($root, 'share', 'site.psgi'),
);
for my $sub (keys %paths) {
my $path = $paths{$sub};
no strict;
*{ $sub } = sub { $path };
}
}
sub import {
my $class = shift;
my @invalid;
for my $param (@_) {
if ($param eq '-inc') {
$class->includePreloads;
}
elsif ($param eq '-preload') {
$class->preloadAll;
}
else {
push @invalid, $param;
}
}
if (@invalid) {
croak 'Invalid options ' . join(', ', @invalid);
}
}
=head2 siteConfigs
Returns the absolute paths of all of the config files inside L</configBase> as an array.
=cut
sub siteConfigs {
my $class = shift;
opendir my $dh, $class->configBase;
my @configs;
while ( my $file = readdir $dh ) {
my $fullPath = realpath( catfile( $class->configBase, $file ) );
if ( -d $fullPath
|| $file !~ /\.conf$/
|| $fullPath eq realpath($class->logConfig)
|| $fullPath eq realpath($class->spectreConfig) )
{
next;
}
push @configs, $fullPath;
}
return @configs;
} ## end sub siteConfigs
=head2 preloadPaths
Returns the list of paths in the preload.custom file as an array.
=cut
sub preloadPaths {
my $class = shift;
my @paths;
try {
for my $path ( _readTextLines($class->preloadCustom) ) {
if (-d $path) {
push @paths, $path;
}
else {
warn "WARNING: Not adding lib directory '$path' from "
. $class->preloadCustom . ": Directory does not exist.\n";
}
}
};
return @paths;
}
=head2 includePreloads
Adds the paths from preload.custom to @INC.
=cut
sub includePreloads {
my $class = shift;
unshift @INC, $class->preloadPaths;
}
=head2 preloadExclude
Returns the list of modules to exclude from preloading as an array.
=cut
sub preloadExclude {
my $class = shift;
my @excludes = _readTextLines($class->preloadExclusions);
push @excludes, 'WebGUI::Upgrade', 'WebGUI::Upgrade::*';
push @excludes, 'WebGUI::Test', 'WebGUI::Test::*';
return @excludes;
}
=head2 preloadAll
Preloads all of the modules in the WebGUI namespace into memory.
=cut
sub preloadAll {
my $class = shift;
$class->includePreloads;
require WebGUI::Pluggable;
my @exclusions = $class->preloadExclude;
WebGUI::Pluggable::findAndLoad( 'WebGUI', {
exclude => \@exclusions,
onLoadFail => sub { warn sprintf "Error loading %s: %s\n", @_ },
});
}
no namespace::clean;
sub _readTextLines {
my $file = shift;
my @lines;
open my $fh, '<', $file or return;
while (my $line = <$fh>) {
$line =~ s/#.*//;
$line =~ s/^\s+//;
$line =~ s/\s+$//;
next
if !$line;
push @lines, $line;
}
return @lines;
}
use namespace::clean;
1;

View file

@ -137,7 +137,7 @@ checkModule('JavaScript::Packer', '1.002' );
checkModule('CSS::Packer', '1.000' );
checkModule('HTML::Packer', "1.000" );
checkModule('Business::Tax::VAT::Validation', '0.20' );
checkModule('Crypt::SSLeay', '0.57' );
#checkModule('Crypt::SSLeay', '0.57' );
checkModule('Scope::Guard', '0.20' );
checkModule('Digest::SHA', '5.47' );
checkModule("CSS::Minifier::XS", "0.03" );

19
sbin/wgd Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/perl
package
App::WGDev;
# ABSTRACT: WebGUI Developer Utilities
use strict;
use warnings;
use 5.008008;
use WGDev::Command;
our $PACKED;
our @PACKED;
my $return = eval { WGDev::Command->run(@ARGV) };
if ( my $message = $@ ) {
$message =~ s/\n?\z/\n/msx;
die $message;
}
exit( $return ? 0 : 1 );

View file

@ -0,0 +1,19 @@
server {
listen 8080 default_server;
access_log /dev/stdout main;
index index.htm index.html;
root /data/WebGUI/www;
location / {
proxy_pass http://webgui-upstream;
}
location /extras/ {
add_header Cache-Control public;
expires 24h;
add_header Access-Control-Allow-Origin *;
}
location /uploads/filepump { expires max; }
}

25
share/nginx-main Normal file
View file

@ -0,0 +1,25 @@
worker_processes 1;
error_log /dev/stdout warn;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
keepalive_timeout 65;
include /etc/nginx/streams.d/*.conf;
include /etc/nginx/conf.d/*.conf;
}

View file

@ -0,0 +1,3 @@
upstream webgui-upstream {
server 127.0.0.1:80;
}

38
www/maintenance.html Normal file
View file

@ -0,0 +1,38 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Site Down For Maintenance</title>
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="Sat, 01 Dec 2001 00:00:00 GMT">
<style type="text/css">
.text {
position: absolute;
z-index: 10;
font-family: georgia, verdana, helvetica, arial, sans-serif;
font-weight: bold;
color: white;
top: 40%;
width: 95%;
text-align: center;
text-shadow: black 2px 2px 2px; /* safari */
filter:progid:DXImageTransform.Microsoft.dropshadow(offX=1, offY=1, color='black', positive='true') /* ie */
}
.header {
font-size: 30px;
}
.body {
background-color: #6974DE;
}
</style>
</head>
<body>
<div class="text">
<div class="header">Maintenance</div>
<p> This site is currently undergoing maintenance. Please check back again shortly. </p>
</div>
<img src="/extras/background.jpg" border="0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 5;" />
</body>
</html>