* Fixed issue caused by setting a Metadata property to a Java value, e.g. java.net.URL instance becomes string without quotes. (Could this be a Rhino bug?) * Finalized conversion of AV_USER table to simple naming scheme ("user") * Added global getTitle() method which returns either site.title or root.sys_title * Implemented universal HopObject.value() method * Rededicated User.update() method since its previous functionality is now taken over by User.value() * Restructured login and register functionalities in User and MemberMgr * Replaced first occurrences of Exception with Error * Introduced i18n via gettext in User and MemberMgr * Removed getMessage() and Message in User and MemberMgr * Added first possible implementation of global getPermission() method * Modified code of global evalEmail() and evalURL() methods to work with Helma modules * Simplified global sendMail() method by not throwing any MailException anymore and returning the status code only * sendMail() now is using helma.Mail (so we can debug message output)
1045 lines
32 KiB
JavaScript
1045 lines
32 KiB
JavaScript
//
|
|
// The Antville Project
|
|
// http://code.google.com/p/antville
|
|
//
|
|
// Copyright 2001-2007 by The Antville People
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the ``License'');
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an ``AS IS'' BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
// $Revision$
|
|
// $LastChangedBy$
|
|
// $LastChangedDate$
|
|
// $URL$
|
|
//
|
|
|
|
/**
|
|
* constructor function for story objects
|
|
*/
|
|
Story.prototype.constructor = function(creator, createtime, ipaddress) {
|
|
this.reads = 0;
|
|
this.ipaddress = ipaddress;
|
|
this.creator = creator;
|
|
this.modifier = creator;
|
|
this.editableby = EDITABLEBY_ADMINS;
|
|
this.createtime = new Date();
|
|
this.modifytime = this.createtime;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* main action
|
|
*/
|
|
Story.prototype.main_action = function() {
|
|
res.data.title = this.site.title;
|
|
var storytitle = this.getRenderedContentPart("title");
|
|
if (storytitle)
|
|
res.data.title += ": " + stripTags(storytitle);
|
|
res.data.body = this.renderSkinAsString("main");
|
|
this.site.renderSkin("page");
|
|
// increment read-counter
|
|
this.incrementReadCounter();
|
|
logAccess();
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* edit action
|
|
*/
|
|
Story.prototype.edit_action = function() {
|
|
// restore any rescued text
|
|
if (session.data.rescuedText)
|
|
restoreRescuedText();
|
|
|
|
if (req.data.set) {
|
|
this.toggleOnline(req.data.set);
|
|
if (req.data.http_referer)
|
|
res.redirect(req.data.http_referer);
|
|
res.redirect(this.site.stories.href());
|
|
} else if (req.data.cancel) {
|
|
res.redirect(this.online ? this.href() : this.site.stories.href());
|
|
} else if (req.data.save || req.data.publish) {
|
|
//try {
|
|
var result = this.evalStory(req.data, session.user);
|
|
res.message = result.toString();
|
|
res.redirect(result.url);
|
|
//} catch (err) {
|
|
res.message = err.toString();
|
|
//}
|
|
}
|
|
|
|
res.data.action = this.href(req.action);
|
|
res.data.title = getMessage("Story.editTitle");
|
|
if (this.title)
|
|
res.data.title += ": " + encode(this.title);
|
|
res.data.body = this.renderSkinAsString("edit");
|
|
this.site.renderSkin("page");
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* delete action
|
|
*/
|
|
Story.prototype.delete_action = function() {
|
|
if (req.data.cancel)
|
|
res.redirect(this.site.stories.href());
|
|
else if (req.data.remove) {
|
|
try {
|
|
res.message = this.site.stories.deleteStory(this);
|
|
res.redirect(this.site.stories.href());
|
|
} catch (err) {
|
|
res.message = err.toString();
|
|
}
|
|
}
|
|
|
|
res.data.action = this.href(req.action);
|
|
res.data.title = getMessage("Story.deleteTitle");
|
|
if (this.title)
|
|
res.data.title += ": " + encode(this.title);
|
|
|
|
if (this.title)
|
|
var skinParam = {
|
|
description: getMessage("Story.deleteDescription"),
|
|
detail: this.title
|
|
};
|
|
else
|
|
var skinParam = {description: getMessage("Story.deleteDescriptionNoTitle")};
|
|
res.data.body = this.renderSkinAsString("delete", skinParam);
|
|
this.site.renderSkin("page");
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* comment action
|
|
*/
|
|
Story.prototype.comment_action = function() {
|
|
// restore any rescued text
|
|
if (session.data.rescuedText)
|
|
restoreRescuedText();
|
|
|
|
if (req.data.cancel)
|
|
res.redirect(this.href());
|
|
else if (req.data.save) {
|
|
try {
|
|
var result = this.evalComment(req.data, session.user);
|
|
res.message = result.toString();
|
|
res.redirect(this.href() + "#" + result.id);
|
|
} catch (err) {
|
|
res.message = err.toString();
|
|
}
|
|
}
|
|
|
|
res.data.action = this.href(req.action);
|
|
res.data.title = this.site.title;
|
|
if (this.title)
|
|
res.data.title += " - " + encode(this.title);
|
|
res.data.body = this.renderSkinAsString("comment");
|
|
this.site.renderSkin("page");
|
|
// increment read-counter
|
|
this.incrementReadCounter();
|
|
return;
|
|
};
|
|
/*
|
|
* macro for rendering a part of the story content
|
|
*/
|
|
Story.prototype.content_macro = function(param) {
|
|
switch (param.as) {
|
|
case "editor" :
|
|
var inputParam = this.content.createInputParam(param.part, param);
|
|
delete inputParam.part;
|
|
if (param.cols || param.rows)
|
|
Html.textArea(inputParam);
|
|
else
|
|
Html.input(inputParam);
|
|
break;
|
|
|
|
case "image" :
|
|
var part = this.content.get(param.part);
|
|
if (part && this.site.images.get(part)) {
|
|
delete param.part;
|
|
renderImage(this.site.images.get(part), param);
|
|
}
|
|
break;
|
|
|
|
default :
|
|
if (param.clipping == null)
|
|
param.clipping = "...";
|
|
var part = this.getRenderedContentPart(param.part, param.as);
|
|
if (!part && param.fallback)
|
|
part = this.getRenderedContentPart(param.fallback, param.as);
|
|
if (param.as == "link") {
|
|
if (this._prototype != "Comment")
|
|
Html.openLink({href: this.href()});
|
|
else
|
|
Html.openLink({href: this.story.href() + "#" + this._id});
|
|
part = part ? part.stripTags() : param.clipping;
|
|
}
|
|
if (!param.limit)
|
|
res.write(part);
|
|
else {
|
|
var stripped = part.stripTags();
|
|
var clipped = stripped.clip(param.limit, param.clipping, param.delimiter);
|
|
if (stripped == clipped)
|
|
res.write(part);
|
|
else
|
|
res.write(clipped);
|
|
}
|
|
if (param.as == "link")
|
|
Html.closeLink();
|
|
}
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* macro rendering online status of story
|
|
*/
|
|
Story.prototype.online_macro = function(param) {
|
|
if (!this.online)
|
|
res.write(param.no ? param.no : "offline");
|
|
else
|
|
res.write(param.yes ? param.yes : "online");
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro rendering createtime of story, either as editor,
|
|
* plain text or as link to the frontpage of the day
|
|
*/
|
|
Story.prototype.createtime_macro = function(param) {
|
|
if (param.as == "editor") {
|
|
if (this.createtime)
|
|
param.value = formatTimestamp(this.createtime, "yyyy-MM-dd HH:mm");
|
|
else
|
|
param.value = formatTimestamp(new Date(), "yyyy-MM-dd HH:mm");
|
|
param.name = "createtime";
|
|
Html.input(param);
|
|
} else if (this.createtime) {
|
|
var text = formatTimestamp(this.createtime, param.format);
|
|
if (param.as == "link" && this.online == 2)
|
|
Html.link({href: path.Site.get(String(this.day)).href()}, text);
|
|
else
|
|
res.write(text);
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro rendering a link to edit
|
|
* if user is allowed to edit
|
|
*/
|
|
Story.prototype.editlink_macro = function(param) {
|
|
if (session.user) {
|
|
try {
|
|
this.checkEdit(session.user, res.data.memberlevel);
|
|
} catch (deny) {
|
|
return;
|
|
}
|
|
Html.openLink({href: this.href("edit")});
|
|
if (param.image && this.site.images.get(param.image))
|
|
renderImage(this.site.images.get(param.image), param);
|
|
else
|
|
res.write(param.text ? param.text : getMessage("generic.edit"));
|
|
Html.closeLink();
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro rendering a link to delete
|
|
* if user is creator of this story
|
|
*/
|
|
Story.prototype.deletelink_macro = function(param) {
|
|
if (session.user) {
|
|
try {
|
|
this.checkDelete(session.user, res.data.memberlevel);
|
|
} catch (deny) {
|
|
return;
|
|
}
|
|
Html.openLink({href: this.href("delete")});
|
|
if (param.image && this.site.images.get(param.image))
|
|
renderImage(this.site.images.get(param.image), param);
|
|
else
|
|
res.write(param.text ? param.text : getMessage("generic.delete"));
|
|
Html.closeLink();
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro renders a link to
|
|
* toggle the online-status of this story
|
|
*/
|
|
Story.prototype.onlinelink_macro = function(param) {
|
|
if (session.user) {
|
|
try {
|
|
this.checkEdit(session.user, res.data.memberlevel);
|
|
} catch (deny) {
|
|
return;
|
|
}
|
|
if (this.online && param.mode != "toggle")
|
|
return;
|
|
delete param.mode;
|
|
param.linkto = "edit";
|
|
param.urlparam = "set=" + (this.online ? "offline" : "online");
|
|
Html.openTag("a", this.createLinkParam(param));
|
|
if (param.image && this.site.images.get(param.image))
|
|
renderImage(this.site.images.get(param.image), param);
|
|
else {
|
|
// currently, only the "set online" text is customizable, since this macro
|
|
// is by default only used in that context outside the story manager.
|
|
if (this.online)
|
|
res.write(getMessage("Story.setOffline"));
|
|
else
|
|
res.write(param.text ? param.text : getMessage("Story.setOnline"));
|
|
}
|
|
Html.closeTag("a");
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro renders a link to the story
|
|
*/
|
|
Story.prototype.viewlink_macro = function(param) {
|
|
if (session.user) {
|
|
try {
|
|
this.checkView(session.user, res.data.memberlevel);
|
|
} catch (deny) {
|
|
return;
|
|
}
|
|
Html.openLink({href: this.href()});
|
|
if (param.image && this.site.images.get(param.image))
|
|
renderImage(this.site.images.get(param.image), param);
|
|
else
|
|
res.write(param.text ? param.text : "view");
|
|
Html.closeLink();
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro rendering link to comments
|
|
*/
|
|
Story.prototype.commentlink_macro = function(param) {
|
|
if (this.discussions && this.site.preferences.get("discussions"))
|
|
Html.link({href: this.href(param.to ? param.to : "comment")},
|
|
param.text ? param.text : "comment");
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* macro renders number of comments
|
|
* options: text to use when no comment
|
|
* text to use when one comment
|
|
* text to use when more than one comment
|
|
* action to link to (default: main)
|
|
*/
|
|
Story.prototype.commentcounter_macro = function(param) {
|
|
if (!this.site.preferences.get("discussions") || !this.discussions)
|
|
return;
|
|
var commentCnt = this.comments.count();
|
|
if (!param.linkto)
|
|
param.linkto = "main";
|
|
var linkParam = this.createLinkParam(param);
|
|
// delete the macro-specific attributes for valid markup output
|
|
delete linkParam.as;
|
|
delete linkParam.one;
|
|
delete linkParam.more;
|
|
delete linkParam.no;
|
|
var linkflag = (param.as == "link" && param.as != "text" ||
|
|
!param.as && commentCnt > 0);
|
|
if (linkflag)
|
|
Html.openTag("a", linkParam);
|
|
if (commentCnt == 0)
|
|
res.write(param.no || param.no == "" ?
|
|
param.no : getMessage("Comment.no"));
|
|
else if (commentCnt == 1)
|
|
res.write(param.one ? param.one : getMessage("Comment.one"));
|
|
else
|
|
res.write(commentCnt + (param.more ?
|
|
param.more : " " + getMessage("Comment.more")));
|
|
if (linkflag)
|
|
Html.closeTag("a");
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro loops over comments and renders them
|
|
*/
|
|
Story.prototype.comments_macro = function(param) {
|
|
var s = this.story ? this.story : this;
|
|
if (!s.site.preferences.get("discussions") || !s.discussions)
|
|
return;
|
|
this.comments.prefetchChildren();
|
|
for (var i=0;i<this.size();i++) {
|
|
var c = this.get(i);
|
|
var linkParam = new Object();
|
|
linkParam.name = c._id;
|
|
Html.openTag("a", linkParam);
|
|
Html.closeTag("a");
|
|
if (c.parent)
|
|
c.renderSkin("reply");
|
|
else
|
|
c.renderSkin("toplevel");
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro checks if user is logged in and not blocked
|
|
* if true, render form to add a comment
|
|
*/
|
|
Story.prototype.commentform_macro = function(param) {
|
|
if (!this.discussions)
|
|
return;
|
|
if (session.user) {
|
|
res.data.action = this.href("comment");
|
|
(new Comment()).renderSkin("edit");
|
|
} else {
|
|
Html.link({href: this.site.members.href("login")},
|
|
param.text ? param.text : getMessage("Comment.loginToAdd"));
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro renders the property of story that defines if
|
|
* other users may edit this story
|
|
*/
|
|
Story.prototype.editableby_macro = function(param) {
|
|
if (param.as == "editor" && (session.user == this.creator || !this.creator)) {
|
|
var options = [EDITABLEBY_ADMINS,
|
|
EDITABLEBY_CONTRIBUTORS,
|
|
EDITABLEBY_SUBSCRIBERS];
|
|
var labels = [getMessage("Story.editableBy.admins"),
|
|
getMessage("Story.editableBy.contributors"),
|
|
getMessage("Story.editableBy.subscribers")];
|
|
delete param.as;
|
|
if (req.data.publish || req.data.save)
|
|
var selValue = !isNaN(req.data.editableby) ? req.data.editableby : null;
|
|
else
|
|
var selValue = this.editableby;
|
|
for (var i=0;i<options.length;i++) {
|
|
Html.radioButton({name: "editableby", value: options[i], selectedValue: selValue});
|
|
res.write(" ");
|
|
res.write(labels[i]);
|
|
res.write(" ");
|
|
}
|
|
} else {
|
|
switch (this.editableby) {
|
|
case 0 :
|
|
res.write(getMessage("Story.editableBy.adminsLong", {siteTitle: path.Site.title}));
|
|
return;
|
|
case 1 :
|
|
res.write(getMessage("Story.editableBy.contributorsLong", {siteTitle: path.Site.title}));
|
|
break;
|
|
case 2 :
|
|
res.write(getMessage("Story.editableBy.subscribersLong", {siteTitle: path.Site.title}));
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro renders a checkbox for enabling/disabling discussions
|
|
* for backwards compatibility this macro also renders a hidden input
|
|
* so that we can check if the checkbox is embedded in story/edit.skin
|
|
*/
|
|
Story.prototype.discussions_macro = function(param) {
|
|
if (!path.Site.preferences.get("discussions"))
|
|
return;
|
|
if (param.as == "editor") {
|
|
var inputParam = this.createCheckBoxParam("discussions", param);
|
|
if ((req.data.publish || req.data.save) && !req.data.discussions)
|
|
delete inputParam.checked;
|
|
Html.checkBox(inputParam);
|
|
} else
|
|
res.write(this.discussions ? getMessage("generic.yes") : getMessage("generic.no"));
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro returns a list of references linking to a story
|
|
* since referrers are asynchronously written to database by scheduler
|
|
* it makes sense to cache them in story.cache.rBacklinks because they
|
|
* won't change until the next referrer-update was done
|
|
* @return String rendered backlinks
|
|
*/
|
|
Story.prototype.backlinks_macro = function(param) {
|
|
// check if scheduler has done a new update of accesslog
|
|
// if not and we have cached backlinks simply return them
|
|
if (this.cache.lrBacklinks >= app.data.lastAccessLogUpdate)
|
|
return this.cache.rBacklinks;
|
|
|
|
var c = getDBConnection("antville");
|
|
var dbError = c.getLastError();
|
|
if (dbError)
|
|
return getMessage("error.database", dbError);
|
|
|
|
// we're doing this with direct db access here
|
|
// (there's no need to do it with prototypes):
|
|
var query = "select ACCESSLOG_REFERRER, count(*) as \"COUNT\" from AV_ACCESSLOG where ACCESSLOG_F_TEXT = " + this._id + " group by ACCESSLOG_REFERRER order by \"COUNT\" desc, ACCESSLOG_REFERRER asc;";
|
|
var rows = c.executeRetrieval(query);
|
|
var dbError = c.getLastError();
|
|
if (dbError)
|
|
return getMessage("error.database", dbError);
|
|
|
|
// we show a maximum of 100 backlinks
|
|
var limit = Math.min((param.limit ? parseInt(param.limit, 10) : 100), 100);
|
|
res.push();
|
|
|
|
var skinParam = new Object();
|
|
var cnt = 0;
|
|
while (rows.next() && cnt <= limit) {
|
|
skinParam.count = rows.getColumnItem("COUNT");
|
|
skinParam.referrer = rows.getColumnItem("ACCESSLOG_REFERRER");
|
|
skinParam.text = skinParam.referrer.clip(50);
|
|
this.renderSkin("backlinkItem", skinParam);
|
|
cnt++;
|
|
}
|
|
rows.release();
|
|
// cache rendered backlinks and set timestamp for
|
|
// checking if backlinks should be rendered again
|
|
skinParam = {referrers: res.pop()};
|
|
if (skinParam.referrers.length > 0)
|
|
this.cache.rBacklinks = this.renderSkinAsString("backlinks", skinParam);
|
|
else
|
|
this.cache.rBacklinks = "";
|
|
this.cache.lrBacklinks = new Date();
|
|
res.write(this.cache.rBacklinks);
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* macro renders a checkbox whether the story is
|
|
* published on the site's front page
|
|
*/
|
|
Story.prototype.addtofront_macro = function(param) {
|
|
if (param.as == "editor") {
|
|
// if we're in a submit, use the submitted form value.
|
|
// otherwise, render the object's value.
|
|
if (req.data.publish || req.data.save) {
|
|
if (!req.data.addToFront)
|
|
delete param.checked;
|
|
} else if (this.online != null && this.online < 2) {
|
|
delete param.checked;
|
|
}
|
|
param.name = "addToFront";
|
|
param.value = 1;
|
|
delete param.as;
|
|
Html.checkBox(param);
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* check if story is ok; if true, save changed story
|
|
* @param Obj Object containing the properties needed for creating a new Story
|
|
* @param Obj User-Object modifying this story
|
|
* @return Obj Object containing two properties:
|
|
* - error (boolean): true if error happened, false if everything went fine
|
|
* - message (String): containing a message to user
|
|
*/
|
|
Story.prototype.evalStory = function(param, modifier) {
|
|
var site = this.site || res.handlers.site;
|
|
|
|
// collect content
|
|
var content = extractContent(param, this.content.get());
|
|
// if all story parts are null, return with error-message
|
|
if (!content.exists) {
|
|
throw new Exception("textMissing");
|
|
}
|
|
// check if the createtime is set in param
|
|
if (param.createtime) {
|
|
try {
|
|
var ctime = param.createtime.toDate("yyyy-MM-dd HH:mm", site.getTimeZone());
|
|
} catch (err) {
|
|
throw new Exception("timestampParse", param.createtime);
|
|
}
|
|
}
|
|
|
|
// re-create day of story with respect to site-timezone
|
|
if (ctime && ctime != this.createtime) {
|
|
this.createtime = ctime;
|
|
}
|
|
|
|
if (!this.day) {
|
|
this.day = this.createtime.format("yyyyMMdd", site.getLocale(),
|
|
site.getTimeZone());
|
|
}
|
|
|
|
// store the new values of the story
|
|
if (param.publish) {
|
|
var newStatus = param.addToFront ? 2 : 1;
|
|
if (!this.online || content.isMajorUpdate) {
|
|
site.lastupdate = new Date();
|
|
}
|
|
this.online = newStatus;
|
|
} else {
|
|
this.online = 0;
|
|
}
|
|
if (content.isMajorUpdate) {
|
|
this.modifytime = new Date();
|
|
}
|
|
// let's keep the title property
|
|
this.title = content.value.title;
|
|
|
|
if (!this.creator || modifier == this.creator) {
|
|
this.editableby = !isNaN(param.editableby) ?
|
|
parseInt(param.editableby, 10) : EDITABLEBY_ADMINS;
|
|
}
|
|
this.discussions = param.discussions ? 1 : 0;
|
|
this.modifier = modifier;
|
|
this.ipaddress = param.http_remotehost;
|
|
|
|
// FIXME: Ugly hack to get back to the StoryMgr
|
|
if (!this.site) {
|
|
return;
|
|
}
|
|
|
|
this.content.set(content.value);
|
|
|
|
// Update tags of the story
|
|
this.setTags(param.tags || param.tag_array)
|
|
|
|
// send e-mail notification
|
|
if (site.isNotificationEnabled() && newStatus != 0) {
|
|
// status changes from offline to online
|
|
// (this is bad because somebody could send a bunch
|
|
// of e-mails simply by toggling the online status.)
|
|
//if (this.online == 0)
|
|
// this.sendNotification("story", "create");
|
|
// major update of an already online story
|
|
if (this.online != 0 && content.isMajorUpdate)
|
|
site.sendNotification("update", this);
|
|
}
|
|
|
|
var result = new Message("storyUpdate");
|
|
result.url = this.online > 0 ? this.href() : site.stories.href();
|
|
result.id = this._id;
|
|
// add the modified story to search index
|
|
app.data.indexManager.getQueue(site).add(this);
|
|
return result;
|
|
};
|
|
|
|
Story.prototype.setTags = function(tags) {
|
|
if (!tags) {
|
|
tags = [];
|
|
} else if (tags.constructor === String) {
|
|
tags = tags.split(/\s*,\s*/);
|
|
}
|
|
|
|
var diff = {};
|
|
var tag;
|
|
for (var i in tags) {
|
|
// Trim and remove URL characters (like ../.. etc.)
|
|
tag = tags[i] = String(tags[i]).trim().replace(/^[\/\.]+$/, "?");
|
|
if (tag && diff[tag] == null) {
|
|
diff[tag] = 1;
|
|
}
|
|
}
|
|
this.tags.forEach(function() {
|
|
diff[this.tag.name] = (tags.indexOf(this.tag.name) < 0) ? this : 0;
|
|
});
|
|
|
|
for (var tag in diff) {
|
|
switch (diff[tag]) {
|
|
case 0:
|
|
// Do nothing (tag already exists)
|
|
break;
|
|
case 1:
|
|
// Add tag to story
|
|
Story.prototype.addTag.call(this, tag);
|
|
break;
|
|
default:
|
|
// Remove tag
|
|
Story.prototype.removeTag.call(this, diff[tag]);
|
|
}
|
|
}
|
|
return;
|
|
};
|
|
|
|
Story.prototype.addTag = function(name) {
|
|
//res.debug("Add tag " + name);
|
|
//return;
|
|
this.tags.add(new TagHub(name, this, session.user));
|
|
return;
|
|
};
|
|
|
|
Story.prototype.removeTag = function(tag) {
|
|
//res.debug("Remove " + tag);
|
|
//return;
|
|
var parent = tag._parent;
|
|
// Remove tag from site if necessary
|
|
if (parent.size() === 1) {
|
|
res.debug("Remove " + parent);
|
|
parent.remove();
|
|
}
|
|
// Remove tag from story
|
|
tag.remove();
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* function sets story either online or offline
|
|
*/
|
|
Story.prototype.toggleOnline = function(newStatus) {
|
|
if (newStatus == "online") {
|
|
this.online = 2;
|
|
this.site.lastupdate = new Date();
|
|
} else if (newStatus == "offline")
|
|
this.online = 0;
|
|
|
|
// add the modified story to search index
|
|
app.data.indexManager.getQueue(this.site).add(this);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* function evaluates comment and adds it if ok
|
|
* @param Obj Object containing properties needed for creation of comment
|
|
* @param Obj Story-Object
|
|
* @param Obj User-Object (creator of comment)
|
|
* @return Obj Object containing two properties:
|
|
* - error (boolean): true if error happened, false if everything went fine
|
|
* - message (String): containing a message to user
|
|
*/
|
|
Story.prototype.evalComment = function(param, creator) {
|
|
// collect content
|
|
var content = extractContent(param);
|
|
if (!content.exists)
|
|
throw new Exception("textMissing");
|
|
var c = new Comment(this.site, creator, param.http_remotehost);
|
|
c.content.setAll(content.value);
|
|
// let's keep the title property:
|
|
c.title = content.value.title;
|
|
this.add(c);
|
|
// also add to story.comments since it has
|
|
// cachemode set to aggressive and wouldn't refetch
|
|
// its child collection index otherwise
|
|
if (this.story)
|
|
this.story.comments.add(c);
|
|
else
|
|
this.comments.add(c);
|
|
this.site.lastupdate = new Date();
|
|
// send e-mail notification
|
|
if (this.site.isNotificationEnabled())
|
|
this.site.sendNotification("create", c);
|
|
var result = new Message("commentCreate");
|
|
result.id = c._id;
|
|
// add the new comment to search index
|
|
app.data.indexManager.getQueue(this.site).add(c);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* function deletes a whole thread
|
|
* @param Obj Comment-Object that should be deleted
|
|
* @return String Message indicating success/failure
|
|
*/
|
|
Story.prototype.deleteComment = function(commentObj) {
|
|
for (var i=commentObj.size();i>0;i--)
|
|
this.deleteComment(commentObj.get(i-1));
|
|
// also remove from comment's parent since it has
|
|
// cachemode set to aggressive and wouldn't refetch
|
|
// its child collection index otherwise
|
|
(commentObj.parent ? commentObj.parent : this).removeChild(commentObj);
|
|
this.comments.removeChild(commentObj);
|
|
commentObj.remove();
|
|
|
|
// remove the modified comment from search index
|
|
app.data.indexManager.getQueue(this.site).remove(commentObj._id);
|
|
return new Message("commentDelete");
|
|
};
|
|
|
|
/**
|
|
* function checks if the text of the story was already cached
|
|
* and if it's still valid
|
|
* if false, it caches it again
|
|
* @return String cached text of story
|
|
*/
|
|
Story.prototype.getRenderedContentPart = function(name, fmt) {
|
|
var part = this.content.get(name);
|
|
if (!part)
|
|
return "";
|
|
var key = fmt ? name + ":" + fmt : name;
|
|
var lastRendered = this.cache["lastRendered_" + key];
|
|
if (!lastRendered) {
|
|
// FIXME: || lastRendered.getTime() < this.content.getLastModified().getTime())
|
|
switch (fmt) {
|
|
case "plaintext":
|
|
part = stripTags(part).clipURLs(30);
|
|
break;
|
|
case "alttext":
|
|
part = stripTags(part);
|
|
part = part.replace(/\"/g, """);
|
|
part = part.replace(/\'/g, "'");
|
|
break;
|
|
default:
|
|
var s = createSkin(format(part));
|
|
this.allowTextMacros(s);
|
|
// enable caching; some macros (eg. poll, storylist)
|
|
// will set this to false to prevent caching of a contentpart
|
|
// containing them
|
|
req.data.cachePart = true;
|
|
// The following is necessary so that global macros know where they belong to.
|
|
// Even if they are embeded at some other site.
|
|
var tmpSite;
|
|
if (this.site != res.handlers.site) {
|
|
tmpSite = res.handlers.site;
|
|
res.handlers.site = this.site;
|
|
}
|
|
part = this.renderSkinAsString(s).activateURLs(50);
|
|
if (tmpSite)
|
|
res.handlers.site = tmpSite;
|
|
}
|
|
this.cache[key] = part;
|
|
if (req.data.cachePart)
|
|
this.cache["lastRendered_" + key] = new Date();
|
|
}
|
|
return this.cache[key];
|
|
};
|
|
|
|
/**
|
|
* function deletes all childobjects of a story (recursive!)
|
|
*/
|
|
Story.prototype.deleteAll = function() {
|
|
var queue = app.data.indexManager.getQueue(this.site);
|
|
var item;
|
|
for (var i=this.comments.size();i>0;i--) {
|
|
item = this.comments.get(i-1);
|
|
// remove comment from search index
|
|
queue.remove(item._id);
|
|
item.remove();
|
|
}
|
|
this.setTags(null);
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* function records the access to a story-object
|
|
* by incrementing the counter of the Object representing
|
|
* this story in app.data.readLog which will be stored
|
|
* in database by scheduler
|
|
*/
|
|
Story.prototype.incrementReadCounter = function() {
|
|
// do not record requests by the story creator
|
|
if (session.user == this.creator)
|
|
return;
|
|
// check if app.data.readLog already contains
|
|
// an Object representing this story
|
|
if (!app.data.readLog.containsKey(String(this._id))) {
|
|
var logObj = new Object();
|
|
logObj.site = this.site.alias;
|
|
logObj.story = this._id;
|
|
logObj.reads = this.reads + 1;
|
|
app.data.readLog.put(String(this._id), logObj);
|
|
} else
|
|
app.data.readLog.get(String(this._id)).reads++;
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* Return either the title of the story or
|
|
* the id prefixed with standard display name
|
|
* to be used in the global linkedpath macro
|
|
* @see hopobject.getNavigationName()
|
|
*/
|
|
Story.prototype.getNavigationName = function() {
|
|
if (this.title)
|
|
return this.title;
|
|
return getDisplay("story") + " " + this._id;
|
|
};
|
|
|
|
|
|
/**
|
|
* creates a Lucene Document object for a story
|
|
* @return Object instance of Search.Document representing the story
|
|
*/
|
|
Story.prototype.getIndexDocument = function() {
|
|
var doc = new Search.Document();
|
|
switch (this._prototype) {
|
|
case "Comment":
|
|
doc.addField("story", this.story._id, {store: true, index: true, tokenize: false});
|
|
if (this.parent)
|
|
doc.addField("parent", this.parent._id, {store: true, index: true, tokenize: false});
|
|
break;
|
|
default:
|
|
doc.addField("day", this.day, {store: true, index: true, tokenize: false});
|
|
if (this.topic)
|
|
doc.addField("topic", this.topic, {store: true, index: true, tokenize: true});
|
|
break;
|
|
}
|
|
|
|
doc.addField("online", this.online, {store: true, index: true, tokenize: false});
|
|
doc.addField("site", this.site._id, {store: true, index: true, tokenize: false});
|
|
doc.addField("id", this._id, {store: true, index: true, tokenize: false});
|
|
var content = this.content.get();
|
|
var title;
|
|
if (title = stripTags(content.title).trim())
|
|
doc.addField("title", title, {store: false, index: true, tokenize: true});
|
|
var text = new java.lang.StringBuffer();
|
|
for (var propName in content) {
|
|
if (propName != "title") {
|
|
text.append(stripTags(content[propName]).trim());
|
|
text.append(" ");
|
|
}
|
|
}
|
|
doc.addField("text", text.toString(), {store: false, index: true, tokenize: true});
|
|
if (this.creator) {
|
|
// FIXME: checking this shouldn't be necessary, but somehow it is ...
|
|
doc.addField("creator", this.creator.name, {store: false, index: true, tokenize: false});
|
|
doc.addField("createtime", this.createtime.format("yyyyMMdd"), {store: false, index: true, tokenize: false});
|
|
}
|
|
return doc;
|
|
};
|
|
/**
|
|
* permission check (called by hopobject.onRequest())
|
|
* @param String name of action
|
|
* @param Obj User object
|
|
* @param Int Membership level
|
|
* @return Obj Exception object or null
|
|
*/
|
|
Story.prototype.checkAccess = function(action, usr, level) {
|
|
var url = this.site.href();
|
|
try {
|
|
switch (action) {
|
|
case "main" :
|
|
this.checkView(usr, level);
|
|
break;
|
|
case "edit" :
|
|
if (!usr && req.data.save)
|
|
rescueText(req.data);
|
|
checkIfLoggedIn(this.href(req.action));
|
|
this.checkEdit(usr, level);
|
|
break;
|
|
case "delete" :
|
|
checkIfLoggedIn();
|
|
this.checkDelete(usr, level);
|
|
break;
|
|
case "comment" :
|
|
if (!usr && req.data.save)
|
|
rescueText(req.data);
|
|
checkIfLoggedIn(this.href(req.action));
|
|
url = this.href();
|
|
this.checkPost(usr, level);
|
|
break;
|
|
}
|
|
} catch (deny) {
|
|
res.message = deny.toString();
|
|
res.redirect(url);
|
|
}
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* check if user is allowed to post a comment to this story
|
|
* @param Obj Userobject
|
|
* @param Int Permission-Level
|
|
* @return String Reason for denial (or null if allowed)
|
|
*/
|
|
Story.prototype.checkPost = function(usr, level) {
|
|
if (!usr.sysadmin && !this.site.online && level == null)
|
|
throw new DenyException("siteView");
|
|
else if (!this.site.preferences.get("discussions"))
|
|
throw new DenyException("siteNoDiscussion");
|
|
else if (!this.discussions)
|
|
throw new DenyException("storyNoDiscussion");
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* check if user is allowed to delete a story
|
|
* @param Obj Userobject
|
|
* @param Int Permission-Level
|
|
* @return String Reason for denial (or null if allowed)
|
|
*/
|
|
Story.prototype.checkDelete = function(usr, level) {
|
|
if (this.creator != usr && (level & MAY_DELETE_ANYSTORY) == 0)
|
|
throw new DenyException("storyDelete");
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* check if user is allowed to edit a story
|
|
* @param Obj Userobject
|
|
* @param Int Permission-Level
|
|
* @return String Reason for denial (or null if allowed)
|
|
*/
|
|
Story.prototype.checkEdit = function(usr, level) {
|
|
if (this.creator != usr) {
|
|
if (level == null)
|
|
throw new DenyException("storyEdit");
|
|
else if (this.editableby == EDITABLEBY_ADMINS && (level & MAY_EDIT_ANYSTORY) == 0)
|
|
throw new DenyException("storyEdit");
|
|
else if (this.editableby == EDITABLEBY_CONTRIBUTORS && (level & MAY_ADD_STORY) == 0)
|
|
throw new DenyException("storyEdit");
|
|
}
|
|
return;
|
|
};
|
|
|
|
|
|
/**
|
|
* check if user is allowed to view story
|
|
* @param Obj Userobject
|
|
* @param Int Permission-Level
|
|
* @return String Reason for denial (or null if allowed)
|
|
*/
|
|
Story.prototype.checkView = function(usr, level) {
|
|
this.site.checkView(usr, level);
|
|
if (!this.online && this.creator != usr) {
|
|
if (this.editableby == EDITABLEBY_ADMINS && (level & MAY_EDIT_ANYSTORY) == 0)
|
|
throw new DenyException("storyView");
|
|
else if (this.editableby == EDITABLEBY_CONTRIBUTORS && (level & MAY_ADD_STORY) == 0)
|
|
throw new DenyException("storyView");
|
|
}
|
|
return;
|
|
};
|
|
|
|
/**
|
|
* function explicitly allowes some macros for use in the text of a story
|
|
* @param Obj Skin-object to allow macros for
|
|
*/
|
|
Story.prototype.allowTextMacros = function(s) {
|
|
s.allowMacro("image");
|
|
s.allowMacro("this.image");
|
|
s.allowMacro("site.image");
|
|
s.allowMacro("story.image");
|
|
s.allowMacro("thumbnail");
|
|
s.allowMacro("this.thumbnail");
|
|
s.allowMacro("site.thumbnail");
|
|
s.allowMacro("story.thumbnail");
|
|
s.allowMacro("link");
|
|
s.allowMacro("this.link");
|
|
s.allowMacro("site.link");
|
|
s.allowMacro("story.link");
|
|
s.allowMacro("file");
|
|
s.allowMacro("poll");
|
|
s.allowMacro("logo");
|
|
s.allowMacro("storylist");
|
|
s.allowMacro("fakemail");
|
|
s.allowMacro("this.topic");
|
|
s.allowMacro("story.topic");
|
|
s.allowMacro("imageoftheday");
|
|
s.allowMacro("spacer");
|
|
|
|
// allow module text macros
|
|
for (var i in app.modules) {
|
|
if (app.modules[i].allowTextMacros)
|
|
app.modules[i].allowTextMacros(s);
|
|
}
|
|
return;
|
|
};
|