# -*- indent-tabs-mode: nil; -*- # vim:ft=perl:et:sw=4 # $Id$ # Sympa - SYsteme de Multi-Postage Automatique # # Copyright (c) 1997, 1998, 1999 Institut Pasteur & Christophe Wolfhugel # Copyright (c) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, # 2006, 2007, 2008, 2009, 2010, 2011 Comite Reseau des Universites # Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016, 2017 GIP RENATER # Copyright 2017, 2018 The Sympa Community. See the AUTHORS.md file at the # top-level directory of this distribution and at # . # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . package Sympa::WWW::SharedDocument; use strict; use warnings; use English qw(-no_match_vars); use File::Find qw(); use POSIX qw(); use Sympa; use Conf; use Sympa::Language; use Sympa::Scenario; use Sympa::Tools::Data; use Sympa::Tools::File; use Sympa::Tools::Text; # Creates a new object. sub new { my $class = shift; my $list = shift; my $path = shift; my %options = @_; die 'bug in logic. Ask developer' unless ref $list eq 'Sympa::List'; my $paths; if (ref $path eq 'ARRAY') { $paths = $path; } elsif (defined $path and length $path) { $paths = [split m{/+}, $path]; } else { $paths = []; } unless (@$paths) { return $class->_new_root($list); } else { my $parent_paths = [@$paths]; my $name = pop @$parent_paths; return undef unless defined $name and length $name and $name !~ /\A[.]+\z/ and $name !~ /\A[.]desc(?:[.]|\z)/; my $parent = $class->new($list, $parent_paths); return undef unless $parent; #FIXME: At present, conversion by qencode_filename() / # qdecode_filename() may not be bijective. So we take the first one # of (possibly multiple) matching paths insted of taking encoded one. my ($self) = $parent->get_children(%options, name => $name); return $self; } } sub _new_root { my $class = shift; my $list = shift; my $status; if (-e $list->{'dir'} . '/shared') { $status = 'exist'; } elsif (-e $list->{'dir'} . '/pending.shared') { $status = 'deleted'; } else { $status = 'none'; } bless { context => $list, fs_name => '', fs_path => $list->{'dir'} . '/shared', name => '', paths => [], status => $status, type => 'root', } => $class; } sub _new_child { my $self = shift; my $fs_name = shift; my %options = @_; # Document isn't a description file. # It exists. # It has non-zero size. return undef if $fs_name =~ /\A[.]+\z/ or $fs_name =~ /\A[.]desc(?:[.]|\z)/; return undef unless -e $self->{fs_path} . '/' . $fs_name; unless (exists $options{allow_empty} and $options{allow_empty}) { return undef unless -s $self->{fs_path} . '/' . $fs_name; } my $child = bless { context => $self->{context}, parent => $self } => (ref $self); my $stem; if ($fs_name =~ /\A[.](.*)[.]moderate\z/) { $stem = $1; $child->{moderate} = 1; } else { $stem = $fs_name; } $child->{fs_name} = $fs_name; $child->{fs_path} = $self->{fs_path} . '/' . $fs_name; $child->{name} = Sympa::Tools::Text::qdecode_filename($stem); $child->{paths} = [@{$self->{paths}}, $child->{name}]; $child->{file_extension} = $1 if $stem =~ /[.](\w+)\z/; $child->{type} = (-d $child->{fs_path}) ? 'directory' : ($child->{file_extension} and $child->{file_extension} eq 'url') ? 'url' : 'file'; if (exists $options{name}) { return undef if $child->{name} ne $options{name}; } if (exists $options{moderate}) { return undef if $child->{moderate} xor $options{moderate}; } ## Check access control #check_access_control($child, $param); # Date. $child->{date_epoch} = Sympa::Tools::File::get_mtime($child->{fs_path}); # Size of the doc. $child->{size} = (-s $child->{fs_path}) / 1000; # Load .desc file unless root directory. my %desc = $child->_load_desc; if (%desc) { $child->{serial_desc} = $desc{serial_desc}; $child->{owner} = $desc{email}; $child->{title} = $desc{title}; $child->{scenario} = {read => $desc{read}, edit => $desc{edit}}; } if (exists $options{owner}) { return undef unless defined $child->{owner}; return undef if $child->{owner} ne $options{owner}; } # File, directory or URL ? my $robot_id = $self->{context}->{'domain'}; if ($child->{type} eq 'url') { $child->{icon} = _get_icon($robot_id, 'url'); if (open my $fh, $child->{fs_path}) { my $url = <$fh>; close $fh; chomp $url; $child->{url} = $url; } if ($child->{name} =~ /\A(.+)[.]url\z/) { $child->{label} = $1; } } elsif ($child->{type} eq 'file') { if ($child->{file_extension} and grep { lc $child->{file_extension} eq $_ } qw(htm html)) { # HTML. $child->{mime_type} = 'text/html'; $child->{html} = 1; $child->{icon} = _get_icon($robot_id, 'text'); } elsif (my $type = Conf::get_mime_type($child->{file_extension} || '')) { $child->{mime_type} = lc $type; # Type of the icon. my $mimet; if (lc $type eq 'application/octet-stream') { $mimet = 'octet-stream'; } else { ($mimet) = split m{/}, $type; } $child->{icon} = _get_icon($robot_id, $mimet) || _get_icon($robot_id, 'unknown'); } else { # Unknown file type. $child->{icon} = _get_icon($robot_id, 'unknown'); } } else { # Directory. $child->{icon} = _get_icon($robot_id, 'folder'); } $child; } sub _load_desc { my $self = shift; my $desc_file = $self->_desc_file; return unless $desc_file and -e $desc_file; my %desc = _load_desc_file($desc_file); $desc{serial_desc} = Sympa::Tools::File::get_mtime($desc_file); return %desc; } # Gets path of property description on physical filesystem. sub _desc_file { my $self = shift; return (-d $self->{fs_path}) ? ($self->{fs_path} . '/.desc') : ($self->{parent}->{fs_path} . '/.desc.' . $self->{fs_name}); } # Old name: Sympa::Tools::WWW::get_desc_file(). #FIXME: Generalize parsing. #FIXME: Lock file. sub _load_desc_file { my $file = shift; my $line; my %hash; open my $fh, '<', $file or return; #FIXME: Check errors. while ($line = <$fh>) { if ($line =~ /^title\s*$/) { # Title of the document while ($line = <$fh>) { last if ($line =~ /^\s*$/); $line =~ /^\s*(\S.*\S)\s*/; $hash{'title'} = $hash{'title'} . $1 . " "; } } if ($line =~ /^creation\s*$/) { # Creation of the document. while ($line = <$fh>) { last if ($line =~ /^\s*$/); if ($line =~ /^\s*email\s*(\S*)\s*/) { $hash{'email'} = $1; } if ($line =~ /^\s*date_epoch\s*(\d*)\s*/) { $hash{'date'} = $1; } } } if ($line =~ /^access\s*$/) { # Access scenarios for the document. while ($line = <$fh>) { last if ($line =~ /^\s*$/); if ($line =~ /^\s*read\s*(\S*)\s*/) { $hash{'read'} = $1; } if ($line =~ /^\s*edit\s*(\S*)\s*/) { $hash{'edit'} = $1; } } } } close $fh; return %hash; } # Hash of the icons linked with a type of file. # Note: Image icons are no longer used by templates. This is kept for # backward compatibility. my %icons = ( 'unknown' => 'unknown.png', 'folder' => 'folder.png', 'current_folder' => 'folder.open.png', 'application' => 'unknown.png', 'octet-stream' => 'binary.png', 'audio' => 'sound1.png', 'image' => 'image2.png', 'text' => 'text.png', 'video' => 'movie.png', 'father' => 'back.png', 'sort' => 'down.png', 'url' => 'link.png', 'left' => 'left.png', 'right' => 'right.png', ); # Old name: Sympa::Tools::WWW::get_icon(). # Note: Image icons are no longer used by templates. This is kept for # backward compatibility. sub _get_icon { my $robot = shift || '*'; my $type = shift; return undef unless defined $icons{$type}; return Conf::get_robot_conf($robot, 'static_content_url') . '/icons/' . $icons{$type}; } sub as_hashref { my $self = shift; my %hash = %$self; $hash{context} = { name => $self->{context}->{'name'}, # Compat. < 6.2.32 host => $self->{context}->{'domain'}, }; $hash{parent} = $self->{parent}->as_hashref if $self->{parent}; $hash{paths} = [@{$self->{paths}}]; # Special items. # The i18n'ed date. $hash{date} = Sympa::Language->instance->gettext_strftime('%d %b %Y %H:%M:%S', localtime $self->{date_epoch}) if defined $self->{date_epoch}; # Path components with trailing slash. $hash{paths_d} = [@{$self->{paths}}]; push @{$hash{paths_d}}, '' if grep { $self->{type} eq $_ } qw(root directory); my @ancestors; my $p = $self->{parent}; while ($p) { unshift @ancestors, { name => $p->{name}, paths => $p->{paths}, paths_d => [@{$p->{paths}}, ''], type => $p->{type}, }; $p = $p->{parent}; } $hash{ancestors} = [@ancestors]; return {%hash}; } # Old name: Sympa::List::create_shared(). sub create { my $self = shift; unless ($self->{type} eq 'root') { $ERRNO = POSIX::EINVAL(); return undef; } return undef unless CORE::mkdir $self->{fs_path}, 0777; $self->{status} = 'exist'; return 1; } sub create_child { my $self = shift; my $new_name = shift; my %options = @_; $options{type} ||= 'directory'; if (not Sympa::WWW::SharedDocument::valid_name($new_name)) { $ERRNO = POSIX::EINVAL(); return undef; } my $new_fs_name = $options{moderate} ? '.' . Sympa::Tools::Text::qencode_filename($new_name) . '.moderate' : Sympa::Tools::Text::qencode_filename($new_name); my $new_fs_path = $self->{fs_path} . '/' . $new_fs_name; my $new_desc_file = ($options{type} eq 'directory') ? $new_fs_path . '/.desc' : $self->{fs_path} . '/.desc.' . $new_fs_name; if ($options{type} eq 'directory') { return undef unless mkdir $new_fs_path, 0777; } else { my $fh; return undef unless open $fh, '>', $new_fs_path; if (exists $options{content} and defined $options{content}) { print $fh $options{content}; } close $fh; } # Creation of a default description file my $fh; return undef unless open $fh, '>', $new_desc_file; print $fh "title\n"; print $fh " \n"; print $fh "\n"; print $fh "creation\n"; print $fh " date_epoch " . time . "\n"; print $fh " email $options{owner}\n"; print $fh "\n"; print $fh "access\n"; print $fh " read $options{scenario}->{read}\n"; print $fh " edit $options{scenario}->{edit}\n"; print $fh "\n"; close $fh; return $self->_new_child($new_fs_name, allow_empty => 1); } sub delete { my $self = shift; unless ($self->{type} eq 'root') { $ERRNO = POSIX::EINVAL(); return undef; } my $list = $self->{context}; return undef unless CORE::rename $self->{fs_path}, $list->{'dir'} . '/pending.shared'; $self->{status} = 'deleted'; return 1; } sub count_children { my $self = shift; my $dh; return undef unless opendir $dh, $self->{fs_path}; my @children = grep { !/\A[.]+\z/ and !/\A[.]desc(?:[.]|\z)/ } sort readdir $dh; closedir $dh; return scalar @children; } sub get_children { my $self = shift; my %options = @_; my $dh; return unless opendir $dh, $self->{fs_path}; #FIXME: Report error. my @children = sort { _by_order($options{order_by}) } grep {$_} map { $self->_new_child($_, %options) } grep { !/\A[.]+\z/ and !/\A[.]desc(?:[.]|\z)/ } sort readdir $dh; closedir $dh; return @children; } # Function which sorts a hash of documents # Sort by various parameters # Old name: by_order() in wwsympa.fcgi. sub _by_order { my $order = shift || 'order_by_doc'; if ($order eq 'order_by_doc') { $a->{name} cmp $b->{name} || $b->{date_epoch} <=> $a->{date_epoch}; } elsif ($order eq 'order_by_author') { $a->{owner} cmp $b->{owner} || $b->{date_epoch} <=> $a->{date_epoch}; } elsif ($order eq 'order_by_size') { $a->{size} <=> $b->{size} || $b->{date_epoch} <=> $a->{date_epoch}; } elsif ($order eq 'order_by_date') { $b->{date_epoch} <=> $a->{date_epoch} || $a->{name} cmp $b->{name}; } else { $a->{name} cmp $b->{name}; } } # OBSOLETED. Never used. sub dump { my $self = shift; my $fd = shift; Sympa::Tools::Data::dump_var($self, 0, $fd); } # OBSOLETED. No longer used. sub dup { my $self = shift; my $copy = {}; foreach my $k (keys %$self) { $copy->{$k} = $self->{$k}; } return $copy; } sub count_moderated_descendants { my $self = shift; return undef unless -d $self->{fs_path}; my $count = 0; File::Find::find( sub { $count++ if !/\A[.]desc([.]|\z)/ and /\A[.].*[.]moderate\z/; }, $self->{fs_path} ); return $count; } # Old name: Sympa::List::get_shared_moderated(). sub get_moderated_descendants { my $self = shift; return unless -e $self->{fs_path}; my @moderated = $self->_get_moderated_descendants; wantarray ? @moderated : \@moderated; } # Old name: Sympa::List::sort_dir_to_get_mod(). sub _get_moderated_descendants { my $self = shift; my @moderated; foreach my $child ($self->get_children) { push @moderated, $child if $child->{moderate}; push @moderated, $child->_get_moderated_descendants if $child->{type} eq 'directory'; } return @moderated; } # Returns a hash with privileges in read, edit, control. ## Regulars # read(/) = default (config list) # edit(/) = default (config list) # control(/) = not defined # read(A/B)= (read(A) && read(B)) || # (author(A) || author(B)) # edit = idem read # control (A/B) : author(A) || author(B) # + (set owner A/B) if (empty directory && # control A) # Arguments: # (\%mode,$path) # if mode->{'read'} control access only for read # if mode->{'edit'} control access only for edit # if mode->{'control'} control access only for control # return the hash ( # $result{'may'}{'read'} == $result{'may'}{'edit'} == $result{'may'}{'control'} if is_author else : # $result{'may'}{'read'} = 0 or 1 (right or not) # $result{'may'}{'edit'} = 0(not may edit) or 0.5(may edit with moderation) or 1(may edit ) : it is not a boolean anymore # $result{'may'}{'control'} = 0 or 1 (right or not) # $result{'reason'}{'read'} = string for authorization_reject.tt2 when may_read == 0 # $result{'reason'}{'edit'} = string for authorization_reject.tt2 when may_edit == 0 # $result{'scenario'}{'read'} = scenario name for the document # $result{'scenario'}{'edit'} = scenario name for the document # Old name: d_access_control() in wwsympa.fcgi, # Sympa::SharedDocument::check_access_control(). sub get_privileges { my $self = shift; my %options = @_; my $mode = $options{mode} || ''; my $sender = $options{sender}; my $auth_method = $options{auth_method}; my $scenario_context = $options{scenario_context} || {}; my $list = $self->{context}; # Result my %result; $result{'reason'} = {}; my $mode_read = (0 <= index $mode, 'read'); my $mode_edit = (0 <= index $mode, 'edit'); my $mode_control = (0 <= index $mode, 'control'); # Control for editing my $may_read = 1; my $why_not_read = ''; my $may_edit = 1; my $why_not_edit = ''; my $is_author = 0; # <=> $may_control # First check privileges on the root shared directory. $result{'scenario'}{'read'} = $list->{'admin'}{'shared_doc'}{'d_read'}{'name'}; $result{'scenario'}{'edit'} = $list->{'admin'}{'shared_doc'}{'d_edit'}{'name'}; # Privileged owner has all privileges. if (Sympa::is_listmaster($list, $sender) or $list->is_admin('privileged_owner', $sender)) { $result{'may'}{'read'} = 1; $result{'may'}{'edit'} = 1; $result{'may'}{'control'} = 1; return %result; } # if not privileged owner if ($mode_read) { my $result = Sympa::Scenario->new($list, 'd_read') ->authz($auth_method, $scenario_context); my $action; if (ref($result) eq 'HASH') { $action = $result->{'action'}; $why_not_read = $result->{'reason'}; } $may_read = ($action =~ /\Ado_it\b/i); } if ($mode_edit) { my $result = Sympa::Scenario->new($list, 'd_edit') ->authz($auth_method, $scenario_context); my $action; if (ref($result) eq 'HASH') { $action = $result->{'action'}; $why_not_edit = $result->{'reason'}; } $action ||= ''; # edit = 0, 0.5 or 1 $may_edit = ($action =~ /\Ado_it\b/i) ? 1 : ($action =~ /\Aeditor\b/i) ? 0.5 : 0; $why_not_edit = '' if $may_edit; } # Only authenticated users can edit files. unless ($sender) { $may_edit = 0; $why_not_edit = 'not_authenticated'; } #if ($mode_control) { # $result{'may'}{'control'} = 0; #} my $current = $self; while ($current and @{$current->{paths}}) { if ($current->{scenario}) { if ($mode_read) { my $result = Sympa::Scenario->new($list, 'd_read', name => $current->{scenario}{read}) ->authz($auth_method, $scenario_context); my $action; if (ref($result) eq 'HASH') { $action = $result->{'action'}; $why_not_read = $result->{'reason'}; } $may_read = $may_read && ($action =~ /\Ado_it\b/i); $why_not_read = '' if $may_read; } if ($mode_edit) { my $result = Sympa::Scenario->new($list, 'd_edit', name => $current->{scenario}{edit}) ->authz($auth_method, $scenario_context); my $action_edit; if (ref($result) eq 'HASH') { $action_edit = $result->{'action'}; $why_not_edit = $result->{'reason'}; } $action_edit ||= ''; # $may_edit = 0, 0.5 or 1 my $may_action_edit = ($action_edit =~ /\Ado_it\b/i) ? 1 : ($action_edit =~ /\Aeditor\b/i) ? 0.5 : 0; $may_edit = !($may_edit and $may_action_edit) ? 0 : ($may_edit == 0.5 or $may_action_edit == 0.5) ? 0.5 : 1; $why_not_edit = '' if $may_edit; } # Only authenticated users can edit files. unless ($sender) { $may_edit = 0; $why_not_edit = 'not_authenticated'; } $is_author = $is_author || (($sender || 'nobody') eq $current->{owner}); unless (defined $result{'scenario'}{'read'}) { $result{scenario}{read} = $current->{scenario}{read}; $result{scenario}{edit} = $current->{scenario}{edit}; } # Author has all privileges. if ($is_author) { $result{'may'}{'read'} = 1; $result{'may'}{'edit'} = 1; $result{'may'}{'control'} = 1; return %result; } } $current = $current->{parent}; } if ($mode_read) { $result{'may'}{'read'} = $may_read; $result{'reason'}{'read'} = $why_not_read; } if ($mode_edit) { $result{'may'}{'edit'} = $may_edit; $result{'reason'}{'edit'} = $why_not_edit; } #if ($mode_control) { # $result{'may'}{'control'} = 0; #} return %result; } # Returns the mode of editing included in $action : 0, 0.5 or 1 # Old name: Sympa::Tools::WWW::find_edit_mode(). # No longer used. #sub _find_edit_mode { # my $action = shift; # # my $result; # if ($action =~ /editor/i) { # $result = 0.5; # } elsif ($action =~ /do_it/i) { # $result = 1; # } else { # $result = 0; # } # return $result; #} # Returns the mode of editing : 0, 0.5 or 1 : # do the merging between 2 args of right access edit : "0" > "0.5" > "1" # instead of a "and" between two booleans : the most restrictive right is # imposed # Old name: Sympa::Tools::WWW::merge_edit(). # No longer used. #sub _merge_edit { # my $arg1 = shift; # my $arg2 = shift; # my $result; # # if ($arg1 == 0 || $arg2 == 0) { # $result = 0; # } elsif ($arg1 == 0.5 || $arg2 == 0.5) { # $result = 0.5; # } else { # $result = 1; # } # return $result; #} # Old name: Sympa::List::get_shared_size(). sub get_size { my $self = shift; return undef unless grep { $self->{type} eq $_ } qw(root directory); return 0 unless -d $self->{fs_path}; return Sympa::Tools::File::get_dir_size($self->{fs_path}); } sub install { my $self = shift; unless ($self->{moderate} and -e $self->{fs_path}) { $ERRNO = POSIX::ENOENT(); return undef; } my $new_fs_name; if ($self->{fs_name} =~ /\A[.](.+)[.]moderate\z/) { $new_fs_name = $1; } else { $ERRNO = POSIX::ENOENT(); return undef; } my $new_fs_path = $self->{parent}->{fs_path} . '/' . $new_fs_name; my $desc_file = $self->_desc_file; my $new_desc_file = (-d $self->{fs_path}) ? ($new_fs_path . '/.desc') : ($self->{parent}->{fs_path} . '/.desc.' . $new_fs_name); # Rename the old file in .old if exists. if (-e $new_fs_path) { return undef unless CORE::rename $new_fs_path, $new_fs_path . '.old'; if (-e $new_desc_file) { return undef unless CORE::rename $new_desc_file, $new_desc_file . '.old'; } } return undef unless CORE::rename $self->{fs_path}, $new_fs_path; if (-e $desc_file) { return undef unless CORE::rename $desc_file, $new_desc_file; } $self->{fs_path} = $new_fs_path; $self->{fs_name} = $new_fs_name; delete $self->{moderate}; return 1; } sub rename { my $self = shift; my $new_name = shift; if ($self->{type} eq 'root') { $ERRNO = POSIX::EPERM(); return undef; } if (not Sympa::WWW::SharedDocument::valid_name($new_name) or ($self->{type} eq 'url' and $new_name !~ /[.]url\z/)) { $ERRNO = POSIX::EINVAL(); return undef; } my $new_fs_name; if ($self->{moderate}) { $new_fs_name = '.' . Sympa::Tools::Text::qencode_filename($new_name) . '.moderate'; } else { $new_fs_name = Sympa::Tools::Text::qencode_filename($new_name); } my $new_fs_path = $self->{parent}->{fs_path} . '/' . $new_fs_name; my $new_paths = [@{$self->{paths}}[0 .. ($#{$self->{paths}} - 1)], $new_name]; return undef unless CORE::rename $self->{fs_path}, $new_fs_path; # Rename description file. unless ($self->{type} eq 'directory') { my $desc_file = $self->_desc_file; my $new_desc_file = $self->{parent}->{fs_path} . '/.desc.' . $new_fs_name; if (-e $desc_file) { return undef unless CORE::rename $desc_file, $new_desc_file; } } @{$self}{qw(fs_name fs_path name paths)} = ($new_fs_name, $new_fs_path, $new_name, $new_paths); return 1; } sub restore { my $self = shift; unless ($self->{type} eq 'root') { $ERRNO = POSIX::EINVAL(); return undef; } my $list = $self->{context}; return undef unless CORE::rename $list->{'dir'} . '/pending.shared', $self->{fs_path}; $self->{status} = 'exist'; return 1; } sub rmdir { my $self = shift; unless ($self->{type} eq 'directory' and -d $self->{fs_path}) { $ERRNO = POSIX::ENOTDIR(); return undef; } if ($self->count_children) { $ERRNO = POSIX::EEXIST(); return undef; } if (-e $self->_desc_file) { return undef unless CORE::unlink $self->_desc_file; } CORE::rmdir $self->{fs_path}; } #FIXME:Generalize serialization. #FIXME:Lock file. sub save_description { my $self = shift; $self->{title} = '' unless defined $self->{title}; my $fh; return undef unless open $fh, '>', $self->_desc_file; print $fh "title\n"; printf $fh " %s\n", $self->{title}; print $fh "\n"; print $fh "access\n"; printf $fh " read %s\n", $self->{scenario}{read}; printf $fh " edit %s\n", $self->{scenario}{edit}; print $fh "\n"; print $fh "creation\n"; printf $fh " date_epoch %s\n", $self->{date_epoch}; printf $fh " email %s\n", $self->{owner}; print $fh "\n"; close $fh; $self->{serial_desc} = Sympa::Tools::File::get_mtime($self->_desc_file); return 1; } sub unlink { my $self = shift; if (grep { $self->{type} eq $_ } qw(root directory)) { $ERRNO = POSIX::EPERM(); return undef; } return undef unless CORE::unlink $self->{fs_path}; my $desc_file = $self->_desc_file; if (-e $desc_file) { return undef unless CORE::unlink $desc_file; } return 1; } sub valid_name { my $new_name = shift; return undef if not defined $new_name or $new_name !~ /\S/ or $new_name =~ /\A[.]/ or 0 <= index($new_name, '/') or $new_name =~ /[<>\\\*\$\[\]\n]/ or $new_name =~ /[~#\[\]]$/; return 1; } # Old name: tools::escape_docname(). # DEPRECATED. No longer used. #sub escape_docname; sub get_id { shift->{fs_path}; } 1; __END__ =encoding utf-8 =head1 NAME Sympa::WWW::SharedDocument - Shared document repository and its nodes =head1 SYNOPSIS use Sympa::WWW::SharedDocument; $shared = Sympa::WWW::SharedDocument->new($list, $path); %access = $shared->get_privileges('read', $email, 'md5', {...}); @children = $shared->get_children; $parent = $shared->{parent}; =head1 DESCRIPTION L implements shared document repository of lists. =head2 Methods =over =item new ( $list, [ $path, [ allow_empty =E 1 ] ] ) I. Creates new instance. Parameters: =over =item $list A L instance. =item $path String to determine path or arrayref of path components. The path is relative to repository root. =item allow_empty =E 1 Don't omit files with zero size. =back Returns: If $path is empty or not specified, returns new instance of repository root; {status} attribute will be set. If $path is not empty and the path exists, returns new instance of node. Otherwise returns false value. =item as_hashref ( ) I. Casts the instance to hashref. Parameters: None. Returns: A hashref including attributes of instance (see L) and following special items: =over =item {ancestors} Arrayref of hashrefs including some attributes of all ancestor nodes. =item {context} Hashref including name and host of the list. =item {date} Localized form of {date_epoch}. =item {parent} Hashref including attributes of parent node recursively. =item {paths_d} Same as {paths} but, if the node is a directory, includes additional empty component at the end. This is useful when the path created by join() should be followed by additional "/" character. =back =item count_children ( ) I. Returns number of child nodes. =item count_moderated_descendants ( ) I. Returns number of nodes waiting for moderation. =item create_child ( $name, owner =E $email, scenario =E $scenario, type =E $type, [ content => $content ] ) I. Creates child node and returns it. TBD. =item get_children ( [ moderate =E boolean ], [ name =E $name ], [ order_by =E $order ], [ owner =E $email ], [ allow_empty =E 1 ] ) I. Gets child nodes. Parameters: =over =item moderate =E boolean =item name =E $name =item owner =E $email Filters results. =item order_by =E $order Sorts results. $order may be one of C<'order_by_doc'> (by name of nodes), C<'order_by_author'> (by owner), C<'order_by_size'> (by size), C<'order_by_date'> (by modification time). Default is ordering by names. =item allow_empty =E 1 Don't omit nodes with zero size. =back Returns: (Possibly empty) list of child nodes. =item get_moderated_descendants ( ) I. Returns the list of nodes waiting for moderation. Parameters: None. Returns: In array context, a list of nodes. In scalar context, an arrayref of them. =item get_privileges ( mode =E $mode, sender =E $sender, auth_method =E $auth_method, scenario_context =E $scenario_context ) I. Gets privileges of a user on the node. TBD. =item get_size ( ) I. Gets total size under current node. =item install ( ) I. Approves (install) file if it was held for moderation. Returns: True value. If installation failed, returns false value and sets $ERRNO ($!). =item rename ( $new_name ) I. Renames file or directory. Parameters: =over =item $new_name The name to be renamed to. =back Returns: True value. If renaming failed, returns false value and sets $ERRNO ($!). =item rmdir ( ) I. Removes directory from repository. Directory must be empty. Returns: True value. If removal failed, returns false value and sets $ERRNO ($!). =item save_description ( ) I. Creates or updates property description of the node. =item unlink ( ) I. Removes file from repository. Returns: True value. If removal failed, returns false value and sets $ERRNO ($!). =item get_id ( ) I. Returns unique identifier of instance. =back =head3 Methods for repository root =over =item create ( ) I. Creates document repository on physical filesystem. =item delete ( ) I. Deletes document repository. =item restore ( ) I. Restores deleted document repository. =back =head2 Functions =over =item valid_name ( $new_name ) I. Check if the name is allowed for directory and file. Note: This should be used with name of newly created node. Existing files and directories may have the name not allowed by this function. =back =head2 Attributes Instance of L may have following attributes. =over =item {context} I. Instance of L class the shared document repository belongs to. =item {date_epoch} I. Modification time of node in Unix time. =item {file_extension} File extension if any. =item {fs_name} I. Name of node on physical filesystem, i.e. the last part of {fs_path}. =item {fs_path} I. Full path of node on physical filesystem. =item {html} Only in HTML file. True value will be set. =item {icon} URL to icon. =item {label} Only in bookmark file. Label to be shown in hyperlink. =item {mime_type} Only in regular file. MIME content type of the file if it is known. =item {moderate} Set if node is held for moderation. =item {name} I. Name of node accessible by users, i.e. the last item of {paths}. =item {owner} Owner (author) of node, given by property description. =item {parent} Parent node if any. L instance. =item {paths} I. Arrayref to all path components of node accessible by users. =item {scenario}{read} =item {scenario}{edit} Scenario names to define privileges. These may be given by property description. =item {serial_desc} Modification time of property description in Unix time. Available if property description exists. =item {size} Size of file. =item {status} I. Status of repository: C<'exist'>, C<'deleted'> or C<'none'>. =item {title} Description of node, given by property description. =item {type} I. Type of node. C<'root'> (the root of repository), C<'directory'> (directory), C<'url'> (bookmark file) or C<'file'> (other file). =item {url} Only in bookmark file. URL to be linked. =back =head1 FILES =over =item I/shared/ Root of repository. =item I<... path>/I Directory or file. =item I<... path>/.I.moderate Moderated directory or file. =item I<... path>/I/.desc =item I<... path>/.desc.I =item I<... path>/.desc..I.moderate Property description of directories or files, not moderated or moderated. =back Note: The path components ("I" above) are encoded to the format suitable to physical filesystem. Such conversion will be hidden behind object methods. =head1 SEE ALSO L, L, L. =head1 HISTORY L module appeared on Sympa 5.2b.2. Rewritten L began to provide OO interface on Sympa 6.2.17. It was renamed to L on Sympa 6.2.26. =cut