package WebGUI::Asset::Post; #------------------------------------------------------------------- # WebGUI is Copyright 2001-2009 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 #------------------------------------------------------------------- use strict; use Tie::CPHash; use Tie::IxHash; use WebGUI::Asset; use WebGUI::Asset::Template; use WebGUI::Asset::Post::Thread; use WebGUI::Cache; use WebGUI::Group; use WebGUI::HTML; use WebGUI::HTMLForm; use WebGUI::Form::DynamicField; use WebGUI::International; use WebGUI::Inbox; use WebGUI::Macro; use WebGUI::Mail::Send; use WebGUI::Operation; use WebGUI::Paginator; use WebGUI::SQL; use WebGUI::Storage; use WebGUI::User; use WebGUI::Utility; use WebGUI::VersionTag; our @ISA = qw(WebGUI::Asset); #------------------------------------------------------------------- =head2 _fixReplyCount ( asset ) Fixes the mismatch in number of replies and lastPost in a thread and/or a CS that occurs after a cut or paste action. Note: if invoked on a thread the CS containing it will very likely be changed as well, but likely in an incorrect manner. Therfore, after running this method on a Thread you probably also want to run it on the container CS. =head3 asset The instanciated asset to fix. This may only be either a WebGUI::Asset::Post::Thread or a WebGUI::Asset::Wobject::Collaboration. =cut sub _fixReplyCount { my $self = shift; my $asset = shift; my $lastPost = $asset->getLineage( [ qw{ self descendants } ], { returnObjects => 1, isa => 'WebGUI::Asset::Post', orderByClause => 'assetData.revisionDate desc', limit => 1, } )->[0]; if ($lastPost) { $asset->incrementReplies( $lastPost->get( 'revisionDate' ), $lastPost->getId ); } else { $asset->incrementReplies( undef, undef ); } } #------------------------------------------------------------------- =head2 addChild ( ) Overriding to limit the types of children allowed. =cut sub addChild { my $self = shift; my $properties = shift; my @other = @_; if ($properties->{className} ne "WebGUI::Asset::Post") { $self->session->errorHandler->security("add a ".$properties->{className}." to a ".$self->get("className")); return undef; } return $self->SUPER::addChild($properties, @other); } #------------------------------------------------------------------- =head2 addRevision ( ) Override the default method in order to deal with attachments. =cut sub addRevision { my $self = shift; my $newSelf = $self->SUPER::addRevision(@_); if ($newSelf->get("storageId") && $newSelf->get("storageId") eq $self->get('storageId')) { my $newStorage = WebGUI::Storage->get($self->session,$self->get("storageId"))->copy; $newSelf->update({storageId=>$newStorage->getId}); } my $threadId = $newSelf->get("threadId"); my $now = time(); if ($threadId eq "") { # new post if ($newSelf->getParent->isa("WebGUI::Asset::Wobject::Collaboration")) { $newSelf->update({threadId=>$newSelf->getId}); } else { $newSelf->update({threadId=>$newSelf->getParent->get("threadId")}); } delete $newSelf->{_thread}; } $newSelf->getThread->unmarkRead; return $newSelf; } #------------------------------------------------------------------- sub canAdd { my $class = shift; my $session = shift; $class->SUPER::canAdd($session, undef, '7'); } #------------------------------------------------------------------- sub canEdit { my $self = shift; my $userId = shift || $self->session->user->userId; my $session = $self->session; my $form = $self->session->form; my $user = WebGUI::User->new( $session, $userId ); # Handle adding new posts if ( ( $form->get("func") eq "add" || ( $form->get("func") eq "editSave" && $form->get("assetId") eq "new" ) ) && $form->get("class") eq "WebGUI::Asset::Post" ) { return $self->getThread->getParent->canPost; } # User who posted can edit their own post if ( $self->isPoster( $userId ) ) { my $editTimeout = $self->getThread->getParent->get( 'editTimeout' ); if ( $editTimeout > time - $self->get( "revisionDate" ) ) { return 1; } } # Users in groupToEditPost of the Collab can edit any post if ( $user->isInGroup( $self->getThread->getParent->get('groupToEditPost') ) ) { return 1; } return $self->getThread->getParent->canEdit( $userId ); } #------------------------------------------------------------------- =head2 canView ( ) Returns a boolean indicating whether the user can view the current post. =cut sub canView { my $self = shift; if (($self->get("status") eq "approved" || $self->get("status") eq "archived") && $self->getThread->getParent->canView) { return 1; } elsif ($self->canEdit) { return 1; } else { $self->getThread->getParent->canEdit; } } #------------------------------------------------------------------- =head2 chopTitle ( ) Cuts a title string off at 30 characters. =cut sub chopTitle { my $self = shift; return substr($self->get("title"),0,30); } #------------------------------------------------------------------- sub commit { my $self = shift; $self->SUPER::commit; $self->notifySubscribers unless ($self->shouldSkipNotification); if ($self->isNew) { if ($self->session->setting->get("useKarma") && $self->getThread->getParent->get("karmaPerPost")) { my $u = WebGUI::User->new($self->session, $self->get("ownerUserId")); $u->karma($self->getThread->getParent->get("karmaPerPost"), $self->getId, "Collaboration post"); } $self->getThread->incrementReplies($self->get("revisionDate"),$self->getId);# if ($self->isReply); } } #------------------------------------------------------------------- sub cut { my $self = shift; # Fetch the Thread and CS before cutting the asset. my $thread = $self->getThread; my $cs = $thread->getParent; # Cut the asset my $result = $self->SUPER::cut; # If a post is being cut update the thread reply count first if ($thread->getId ne $self->getId) { $self->_fixReplyCount( $thread ); } # Update the CS reply count. This step is also necessary when a Post is cut since the Thread's incrementReplies # also calls the CS's incrementReplies, possibly with the wrong last post Id. $self->_fixReplyCount( $cs ); return $result; } #------------------------------------------------------------------- sub definition { my $class = shift; my $session = shift; my $definition = shift; my $i18n = WebGUI::International->new($session,"Asset_Post"); my $properties = { storageId => { fieldType=>"image", defaultValue=>'', enforceSizeLimits => 0, }, threadId => { noFormPost=>1, fieldType=>"hidden", defaultValue=>'', }, originalEmail => { noFormPost=>1, fieldType=>"hidden", defaultValue=>undef }, username => { fieldType=>"hidden", defaultValue=>$session->form->process("visitorUsername") || $session->user->profileField("alias") || $session->user->username }, rating => { noFormPost=>1, fieldType=>"hidden", defaultValue=>undef }, views => { noFormPost=>1, fieldType=>"hidden", defaultValue=>undef }, contentType => { fieldType=>"contentType", defaultValue=>"mixed" }, userDefined1 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined2 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined3 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined4 => { fieldType=>"HTMLArea", defaultValue=>undef }, userDefined5 => { fieldType=>"HTMLArea", defaultValue=>undef }, content => { fieldType=>"HTMLArea", defaultValue=>undef }, }; push(@{$definition}, { assetName=>$i18n->get('assetName'), icon=>'post.gif', tableName=>'Post', className=>'WebGUI::Asset::Post', properties=>$properties, }); return $class->SUPER::definition($session,$definition); } #------------------------------------------------------------------- sub DESTROY { my $self = shift; $self->{_thread}->DESTROY if (exists $self->{_thread} && ref $self->{_thread} =~ /Thread/); $self->SUPER::DESTROY; } #------------------------------------------------------------------- =head2 exportAssetData ( ) See WebGUI::AssetPackage::exportAssetData() for details. =cut sub exportAssetData { my $self = shift; my $data = $self->SUPER::exportAssetData; push(@{$data->{storage}}, $self->get("storageId")) if ($self->get("storageId") ne ""); return $data; } #------------------------------------------------------------------- =head2 fixUrl ( url ) Extends superclass method to remove periods from post urls =head3 url The url of the post =cut sub fixUrl { my $self = shift; my $url = shift; $url =~ s/\./_/g; $self->SUPER::fixUrl($url); } #------------------------------------------------------------------- =head2 formatContent ( [ content, contentType ]) Formats post content for display. =head3 content The content to format. Defaults to the content in this post. =head3 contentType The content type to use for formatting. Defaults to the content type specified in this post. =cut sub formatContent { my $self = shift; my $content = shift || $self->get("content"); my $contentType = shift || $self->get("contentType"); my $msg = WebGUI::HTML::filter($content,$self->getThread->getParent->get("filterCode")); $msg = WebGUI::HTML::format($msg, $contentType); if ($self->getThread->getParent->get("useContentFilter")) { $msg = WebGUI::HTML::processReplacements($self->session,$msg); } return $msg; } #------------------------------------------------------------------- sub getAutoCommitWorkflowId { my $self = shift; my $cs = $self->getThread->getParent; if ($cs->hasBeenCommitted) { return $cs->get('approvalWorkflow') || $self->session->setting->get('defaultVersionTagWorkflow'); } return undef; } #------------------------------------------------------------------- =head2 getAvatarUrl ( ) Returns a URL to the owner's avatar. =cut sub getAvatarUrl { my $self = shift; my $parent = $self->getThread->getParent; return '' unless $parent and $parent->getValue("avatarsEnabled"); my $user = WebGUI::User->new($self->session, $self->get('ownerUserId')); #Get avatar field, storage Id. my $storageId = $user->profileField("avatar"); return '' unless $storageId; my $avatar = WebGUI::Storage->get($self->session,$storageId); my $avatarUrl = ''; if ($avatar) { #Get url from storage object. foreach my $imageName (@{$avatar->getFiles}) { if ($avatar->isImage($imageName)) { $avatarUrl = $avatar->getUrl($imageName); last; } } } return $avatarUrl; } #------------------------------------------------------------------- =head2 getDeleteUrl ( ) Formats the url to delete a post. =cut sub getDeleteUrl { my $self = shift; return $self->getUrl("func=delete;revision=".$self->get("revisionDate")); } #------------------------------------------------------------------- =head2 getEditUrl ( ) Formats the url to edit a post. =cut sub getEditUrl { my $self = shift; return $self->getUrl("func=edit;revision=".$self->get("revisionDate")); } #------------------------------------------------------------------- sub getImageUrl { my $self = shift; return undef if ($self->get("storageId") eq ""); my $storage = $self->getStorageLocation; my $url; foreach my $filename (@{$storage->getFiles}) { if ($storage->isImage($filename)) { $url = $storage->getUrl($filename); last; } } return $url; } #------------------------------------------------------------------- =head2 getPosterProfileUrl ( ) Formats the url to view a users profile. =cut sub getPosterProfileUrl { my $self = shift; return WebGUI::User->new($self->session,$self->get("ownerUserId"))->getProfileUrl; } #------------------------------------------------------------------- =head2 getRateUrl ( rating ) Formats the url to rate a post. =head3 rating An integer between 1 and 5 (5 = best). =cut sub getRateUrl { my $self = shift; my $rating = shift; return $self->getUrl("func=rate;rating=".$rating."#id".$self->getId); } #------------------------------------------------------------------- =head2 getReplyUrl ( [ withQuote ] ) Formats the url to reply to a post. =head3 withQuote If specified the reply with automatically quote the parent post. =cut sub getReplyUrl { my $self = shift; my $withQuote = shift || 0; return $self->getUrl("func=add;class=WebGUI::Asset::Post;withQuote=".$withQuote); } #------------------------------------------------------------------- sub getStatus { my $self = shift; my $status = $self->get("status"); my $i18n = WebGUI::International->new($self->session,"Asset_Post"); if ($status eq "approved") { return $i18n->get('approved'); } elsif ($status eq "pending") { return $i18n->get('pending'); } elsif ($status eq "archived") { return $i18n->get('archived'); } } #------------------------------------------------------------------- sub getStorageLocation { my $self = shift; unless (exists $self->{_storageLocation}) { if ($self->get("storageId") eq "") { $self->{_storageLocation} = WebGUI::Storage->create($self->session); $self->update({storageId=>$self->{_storageLocation}->getId}); } else { $self->{_storageLocation} = WebGUI::Storage->get($self->session,$self->get("storageId")); } } return $self->{_storageLocation}; } #------------------------------------------------------------------- sub getSynopsisAndContent { my $self = shift; my $synopsis = shift; my $body = shift; unless ($synopsis) { my @content; if( $body =~ /\^\-\;/ ) { @content = split(/\^\-\;/, $body ,2); } elsif( $body =~ /
/ ) {
@content = WebGUI::HTML::splitTag($body);
}
else {
@content = split("\n",$body);
}
shift @content if $content[0] =~ /^\s*$/;
$synopsis = WebGUI::HTML::filter($content[0],"all");
}
return ($synopsis,$body);
}
#-------------------------------------------------------------------
sub getTemplateMetadataVars {
my $self = shift;
my $var = shift;
if ($self->session->setting->get("metaDataEnabled")
&& $self->getThread->getParent->get('enablePostMetaData')) {
my $meta = $self->getMetaDataFields();
my @meta_loop = ();
foreach my $field (keys %{ $meta }) {
push @meta_loop, {
value => $meta->{$field}{value},
name => $meta->{$field}{fieldName},
};
my $fieldName = $meta->{$field}{fieldName};
$fieldName =~ tr/ /_/;
$fieldName = lc $fieldName;
$var->{'meta_'.$fieldName.'_value'} = $meta->{$field}{value}; ##By name interface
}
$var->{meta_loop} = \@meta_loop;
}
}
#-------------------------------------------------------------------
sub getTemplateVars {
my $self = shift;
my %var = %{$self->get};
$var{"userId"} = $self->get("ownerUserId");
$var{"user.isPoster"} = $self->isPoster;
$var{"avatar.url"} = $self->getAvatarUrl;
$var{"userProfile.url"} = $self->getUrl("op=viewProfile;uid=".$self->get("ownerUserId"));
$var{"dateSubmitted.human"} =$self->session->datetime->epochToHuman($self->get("creationDate"));
$var{"dateUpdated.human"} =$self->session->datetime->epochToHuman($self->get("revisionDate"));
$var{'title.short'} = $self->chopTitle;
$var{content} = $self->formatContent if ($self->getThread);
$var{'user.canEdit'} = $self->canEdit if ($self->getThread);
$var{"delete.url"} = $self->getDeleteUrl;
$var{"edit.url"} = $self->getEditUrl;
$var{"status"} = $self->getStatus;
$var{"reply.url"} = $self->getReplyUrl;
$var{'reply.withquote.url'} = $self->getReplyUrl(1);
$var{'url'} = $self->getUrl.'#id'.$self->getId;
$var{'url.raw'} = $self->getUrl;
$var{'rating.value'} = $self->get("rating")+0;
$var{'rate.url.thumbsUp'} = $self->getRateUrl(1);
$var{'rate.url.thumbsDown'} = $self->getRateUrl(-1);
$var{'hasRated'} = $self->hasRated;
my $gotImage;
my $gotAttachment;
@{$var{'attachment_loop'}} = ();
unless ($self->get("storageId") eq "") {
my $storage = $self->getStorageLocation;
foreach my $filename (@{$storage->getFiles}) {
if (!$gotImage && $storage->isImage($filename)) {
$var{"image.url"} = $storage->getUrl($filename);
$var{"image.thumbnail"} = $storage->getThumbnailUrl($filename);
$gotImage = 1;
}
if (!$gotAttachment && !$storage->isImage($filename)) {
$var{"attachment.url"} = $storage->getUrl($filename);
$var{"attachment.icon"} = $storage->getFileIconUrl($filename);
$var{"attachment.name"} = $filename;
$gotAttachment = 1;
}
push(@{$var{"attachment_loop"}}, {
url=>$storage->getUrl($filename),
icon=>$storage->getFileIconUrl($filename),
filename=>$filename,
thumbnail=>$storage->getThumbnailUrl($filename),
isImage=>$storage->isImage($filename)
});
}
}
$self->getTemplateMetadataVars(\%var);
return \%var;
}
#-------------------------------------------------------------------
=head2 getThread
Returns the Thread that this Post belongs to. The method caches the result of the Asset creation.
=cut
sub getThread {
my $self = shift;
unless (defined $self->{_thread}) {
my $threadId = $self->get("threadId");
if ($threadId eq "") { # new post
if ($self->getParent->isa("WebGUI::Asset::Wobject::Collaboration")) {
$threadId=$self->getId;
} else {
$threadId=$self->getParent->get("threadId");
}
}
$self->{_thread} = WebGUI::Asset::Post::Thread->new($self->session, $threadId);
}
return $self->{_thread};
}
#-------------------------------------------------------------------
sub getThumbnailUrl {
my $self = shift;
return undef if ($self->get("storageId") eq "");
my $storage = $self->getStorageLocation;
my $url;
foreach my $filename (@{$storage->getFiles}) {
if ($storage->isImage($filename)) {
$url = $storage->getThumbnailUrl($filename);
last;
}
}
return $url;
}
#-------------------------------------------------------------------
=head2 hasRated ( )
Returns a boolean indicating whether this user has already rated this post.
=cut
sub hasRated {
my $self = shift;
return 1 if $self->isPoster;
my $flag = 0;
if ($self->session->user->isVisitor) {
($flag) = $self->session->db->quickArray("select count(*) from Post_rating where assetId=? and ipAddress=?",[$self->getId, $self->session->env->getIp]);
} else {
($flag) = $self->session->db->quickArray("select count(*) from Post_rating where assetId=? and userId=?",[$self->getId, $self->session->user->userId]);
}
return $flag;
}
#-------------------------------------------------------------------
=head2 indexContent ( )
Indexing the content of attachments and user defined fields. See WebGUI::Asset::indexContent() for additonal details.
=cut
sub indexContent {
my $self = shift;
my $indexer = $self->SUPER::indexContent;
$indexer->addKeywords($self->get("content"));
$indexer->addKeywords($self->get("userDefined1"));
$indexer->addKeywords($self->get("userDefined2"));
$indexer->addKeywords($self->get("userDefined3"));
$indexer->addKeywords($self->get("userDefined4"));
$indexer->addKeywords($self->get("userDefined5"));
$indexer->addKeywords($self->get("username"));
my $storage = $self->getStorageLocation;
foreach my $file (@{$storage->getFiles}) {
$indexer->addFile($storage->getPath($file));
}
}
#-------------------------------------------------------------------
=head2 incrementViews ( )
Increments the views counter for this post.
=cut
sub incrementViews {
my ($self) = @_;
$self->update({views=>$self->get("views")+1});
}
#-------------------------------------------------------------------
=head2 insertUserPostRating ( rating )
Register the user's rating against this post.
=head3 rating
An integer indicating either thumbss up (+1) or thumbs down (-1)
=cut
sub insertUserPostRating {
my $self = shift;
my $rating = shift;
return undef unless ($rating == -1 || $rating == 1);
return undef if $self->hasRated;
$self->session->db->write("insert into Post_rating (assetId,userId,ipAddress,dateOfRating,rating) values (?,?,?,?,?)",
[$self->getId,
$self->session->user->userId,
$self->session->env->getIp,
$self->session->datetime->time(),
$rating,]
);
}
#-------------------------------------------------------------------
=head2 isNew ( )
Returns a boolean indicating whether this post is new (not an edit).
=cut
sub isNew {
my $self = shift;
return $self->get("creationDate") == $self->get("revisionDate");
}
#-------------------------------------------------------------------
=head2 isPoster ( userId )
Returns a boolean that is true if the current user created this post and is not a visitor.
=cut
sub isPoster {
my $self = shift;
my $userId = shift || $self->session->user->userId;
return ( $userId ne "1" && $userId eq $self->get("ownerUserId") );
}
#-------------------------------------------------------------------
=head2 isReply ( )
Returns a boolean indicating whether this post is a reply.
=cut
sub isReply {
my $self = shift;
return $self->getId ne $self->get("threadId");
}
#-------------------------------------------------------------------
=head2 notifySubscribers ( )
Send notifications to the thread and forum subscribers that a new post has been made.
=cut
sub notifySubscribers {
my $self = shift;
my $i18n = WebGUI::International->new($self->session);
my $var = $self->getTemplateVars();
my $thread = $self->getThread;
my $cs = $thread->getParent;
$cs->appendTemplateLabels($var);
$var->{relativeUrl} = $var->{url};
my $siteurl = $self->session->url->getSiteURL();
$var->{url} = $siteurl.$self->getUrl;
$var->{'notify.subscription.message'} = $i18n->get(875,"Asset_Post");
my $user = WebGUI::User->new($self->session, $self->get("ownerUserId"));
my $setting = $self->session->setting;
my $returnAddress = $setting->get("mailReturnPath");
my $companyAddress = $setting->get("companyEmail");
my $listAddress = $cs->get("mailAddress");
my $posterAddress = $user->getProfileFieldPrivacySetting('email') eq "all"
? $user->profileField('email')
: '';
my $from = $posterAddress || $listAddress || $companyAddress;
my $replyTo = $listAddress || $returnAddress || $companyAddress;
my $sender = $listAddress || $companyAddress || $posterAddress;
my $returnPath = $returnAddress || $sender;
my $listId = $sender;
$listId =~ s/\@/\./;
my $domain = $cs->get("mailAddress");
$domain =~ s/.*\@(.*)/$1/;
my $messageId = "cs-".$self->getId.'@'.$domain;
my $replyId = "";
if ($self->isReply) {
$replyId = "cs-".$self->getParent->getId.'@'.$domain;
}
my $subject = $cs->get("mailPrefix").$self->get("title");
foreach my $subscriptionAsset ($cs, $thread) {
$var->{unsubscribeUrl} = $siteurl.$subscriptionAsset->getUnsubscribeUrl;
$var->{unsubscribeLinkText} = $i18n->get("unsubscribe","Asset_Collaboration");
my $message = $self->processTemplate($var, $cs->get("notificationTemplateId"));
WebGUI::Macro::process($self->session, \$message);
my $groupId = $subscriptionAsset->get('subscriptionGroupId');
my $mail = WebGUI::Mail::Send->create($self->session, {
from=>"<".$from.">",
returnPath => "<".$returnPath.">",
replyTo=>"<".$replyTo.">",
toGroup=>$groupId,
subject=>$subject,
messageId=>'<'.$messageId.'>'
});
if ($self->isReply) {
$mail->addHeaderField("In-Reply-To", "<".$replyId.">");
$mail->addHeaderField("References", "<".$replyId.">");
}
$mail->addHeaderField("List-ID", $cs->getTitle." <".$listId.">");
$mail->addHeaderField("List-Help", " \n\n --- (".$i18n->get('Edited_on')." ".$self->session->datetime->epochToHuman(undef,"%z %Z [GMT%O]")." ".$i18n->get('By')." ".$user->profileField("alias").") --- \n