User generated content (UGC)
Since OpenCms 9.5 content can be edited and added via a JavaScript API. We call this feature “User-generated content”, or 'UGC' for short. It allows
- Users to generate/edit content without login
- Adding/editing of content from the online project
- The provision of completely customized ways to fill and edit content
- The use of client-side validation of content that goes beyond the validation rules in XSDs
- Building a back-end to an existing form that creates/alters OpenCms XML content
There is nearly no restriction on how you fill content or display content elements. Only with file uploads, are you tied to a form.
Using UGC, you can for example easily create:
- A blog
- A guest book
- Any kind of forms
- and more
UGC allows you to create and manipulate content from the online project. You can map values stored in XML content into a form and update the XML content with the form's values. Communication between the content and the form is session based. For security reasons, sessions are established at the server-side. Hence, you can control who can add or edit which content via which form.
To connect form and content in an established session, a JavaScript API is provided. To reduce the effort in writing lengthy mappings, a jQuery based wrapper for the native API is additionally available. It allows high level mapping operations of content elements and form fields. These operations capture the most frequent use cases.
Configuration of a UGC session
Each UGC session is initialized according to a given UGC configuration, i.e., XML content of type ugc_config
. The configuration allows various settings and can also be used to render the input forms used in a UGC session.
Creating configuration content
We recommend rendering input forms used for UGC via formatters for the UGC configuration content type (type ugc_config
). The advantage is that the configuration used to initialize the session is directly provided by the rendered content. Furthermore, you do not need to create a new content type just to render a form. (A dynamic function is not very suitable because of the missing facility for head-includes.)
Of course, depending on the content type for which content should be added or manipulated via UGC, the formatter must be very different. You can write many formatters and, depending on the content type for which content is accessed, choose the correct formatter via an element setting.
To add an input form for UGC to your page, you just add content of type ugc_config
(named "Form configuration for user-generated content"), edit it to configure UGC and choose the according formatter via an element setting. The formatter should manage the the server-side preparation for UGC and render the input form.
Editing the configuration content, you can or even must provide the following configurations.
General settings
- Content type
The content type for which contents should be edited/added.
- Content folder
The folder where content is loaded from, created in and stored to. For security reasons all operations on content are limited to this single folder.
- Name pattern
The name pattern, defining the names given to newly created contents. The pattern has to use the macro %(number)
, e.g., “ugc_%(number).xml
”.
- Locale
The locale in which the content is read/written.
- Guest user substitute
The name of the user that performs the content access when a guest user uses UGC. Users that are not logged in, i.e., guest users, have no permissions to access/edit/add content in OpenCms. Thus, to allow guests to use UGC, the access to the content must be performed by another user with the according permissions. If you do not set a user here, guests simply cannot perform UGC actions.
- Automatic publishing
If checked, new contents and alterations to existing contents via UGC are published directly. Otherwise, the changes remain unpublished until a user from the configured “Project group” publishes the changes.
- Project group
Group of users that can publish changes made via UGC. The changes are performed in a special project for each session. If “Automatic publishing” in this configuration file is not checked, the project is not published directly. Thus, a user with according permissions must publish the project.
Settings for file uploads
- Upload folder
Specifies the folder to which files are uploaded.
- Max. upload size
If set, the size (in bytes) of each file that can be uploaded is limited to the set value.
- Valid extensions
A list of file extensions separated by “;”. If set, only files with listed extensions can be uploaded. Otherwise, there is no restriction on file extensions.
Security settings preventing DoS-Attacks
- Max. number of contents
Specifies the maximum number of content items allowed in the configured “Content folder”. If not set, the number is not limited.
- Queue wait time
The delay time between generating new sessions (in milliseconds).
- Queue maximum length
The maximum number of sessions that wait for creation. If more session requests are present at the same time, they are ignored.
Server-side preparation for a UGC session
To use the JavaScript API minimal server-side preparation is necessary:
- Include the JavaScript file
ugc.nocache.js
via a headinclude into the formatter that renders the form. The JavaScript file is shipped with the module org.opencms.ugc
and located in the module's jar. The file exposes the UGC JavaScript API.
Optionally, you can include the file ugc-jquery.js
, found in the module's jar as well. It exposes an easy to use wrapper for the native JavaScript API.
Here is an example formatter configuration that includes both JavaScript files plus an extra file that contains the form-specific JavaScript, using the API:
Headincludes in a formatter configuration for a UGC connected form.
- In the formatter that renders the form: Use the tag
<cms:ugc>
to create a UGC session and to expose the retrieved session-id in the HTML code by adding it as an attribute to the <form>
-tag.
Here is an example formatter that does the preparation:
<%@page buffer="none" session="false" trimDirectiveWhitespaces="true" taglibs="c,cms" %>
<cms:formatter var="content">
<div>
<%-- add some case distinction to show something else for new or currently edited contents --%>
<%-- IMPORTANT: Here a UGC-session is initialized. The config path links to the UGC configuration file,
-- the editId to the XML content that should be connected with the form (the UUID must be given),
-- and var is used to expose the session-id of the established session
-- If no editId is given (empty string), a new content is created --%>
<cms:ugc var="ugcId" editId="${param.fileId}" configPath="${content.filename}" />
<%-- render the form --%>
<form
id="ugcForm"
ugc-id="${ugcId}" <%-- IMPORTANT: expose the session-id of the UGC session --%>
method="post"
role="form">
<%-- render the form fields and buttons to save and/or (server-side) validate the input and/or cancel --%>
</form>
</div>
</cms:formatter>
- In the formatter that renders the form: Use the tag
<cms:ugc>
to create a UGC session and to expose the retrieved session-id in the HTML code by adding it as an attribute to the <form>
-tag.
Here is an example formatter that does the preparation:
<%@page buffer="none" session="false" trimDirectiveWhitespaces="true" taglibs="c,cms" %>
<cms:formatter var="content">
<div>
<%-- add some case distinction to show something else for new or currently edited contents --%>
<%-- IMPORTANT: Here a UGC-session is initialized. The config path links to the UGC configuration file,
-- the editId to the XML content that should be connected with the form (the UUID must be given),
-- and var is used to expose the session-id of the established session
-- If no editId is given (empty string), a new content is created --%>
<cms:ugc var="ugcId" editId="${param.fileId}" configPath="${content.filename}" />
<%-- render the form --%>
<form
id="ugcForm"
ugc-id="${ugcId}" <%-- IMPORTANT: expose the session-id of the UGC session --%>
method="post"
role="form">
<%-- render the form fields and buttons to save and/or (server-side) validate the input and/or cancel --%>
</form>
</div>
</cms:formatter>
In the formatter, only two lines are special: The use of the <cms:ugc>
to initialize the session and the parameter at the <form>
tag. Note that this serves equally well for editing existing content (when providing the UUID of the content to edit via the fileId
parameter) or creating a new content (when not providing a fileId
parameter).
The <cms:ugc>
tag will most probably be used as in the above example, but it can be used with slightly different attributes.
To set up a session for newly created content, you can omit the editId
attribute:
<cms:ugc var="sessionId" configPath="${content.filename}" />
The tag creates a new session and exposes the session-id via the variable set in the attribute var
. If var
is not given, the session-id is exposed via the variable formSessionId
.
The attribute configPath
is mandatory. Here the OpenCms VFS URI of the UGC configuration content that should be used to set up the session is given. Value ${content.filename}
will yield the according URI if you use the tag in the formatter of a UGC configuration. This is recommended.
To create a session for editing existing content use the tag as in the above example:
<cms:ugc var="sessionId" configPath="${content.filename}" editId="${param.fileId}" />
The only difference in creating a session for new content is: the file that should be accessed has to be specified. This is either done via the attribute editId
, as in the example, or via the attribute editName
. Using editId
, you must provide the UUID of the content to access it. Using editName
, you must provide the file name. The path is fixed via the UGC configuration content./p>
Session creation might fail. In this case errors are exposed to the JSP. If you add the attribute error="ugcError"
to the <cms:ugc>
-tag, the error is exposed via the given variable ugcError
. If the error
attribute is not given, the error is exposed via the variable formError
.
Using the object-oriented jQuery wrapper for the JavaScript API
For connecting a web form and XML content via UGC you can either use the native JavaScript API, or the object-oriented, jQuery-based wrapper for this API. The wrapper provides a more abstract view of the connection between form and content. Thus, it probably helps to implement your own UGC connection more easily than with the native API.
The wrapper has in particular the following advantages:
- You set up just one mapping relation between content elements and form fields and you can use it for getting values from the content and writing values to the content.
- The UGC object provided by the wrapper takes care of the mapping relation.
- The UGC object provides several useful methods to manipulate the mapped values.
To use the wrapper, include it the respective JavaScript into your formatter as headinclude (see here). Then instantiate your own UGC object and use the object for all UGC specific actions. We recommend making up a new namespace for your UGC code by wrapping it with $(function() { /* ... */ });
.
In the following, we explain how to work with the UGC object. Note that we use the function names from the initialization section to refer to the respective callback or event handlers later on.
A complete example on how to use the object is shipped with the demo, where UGC is used to allow posting blog entries.
Initializing the UGC object
An UGC object is created via
var ugc = new UGC("ugcForm", myMappings, myErrorHandler, myFormInitHandler, myWaitHandler);
where the arguments of the constructor are:
- the
id
attribute of the <form>
that should be connected to the content,
- a function that initializes the mapping between content elements and form fields,
- a handler for server-side errors,
- (optional) a handler called when the form is initialized (i.e., when content elements are written to the form)
- (optional) a wait handler, called when data are sent/received and when sending/receiving is completed.
Initialization must be wrapped in the function window.initUgc
that is called when the native API is ready:
window.initUgc = function() {
if (ugc.initForm()) {
// read the variables from the form HTML
var sessionId = ugc.getForm().attr("ugc-id");
backLink = ugc.getForm().attr("back-link");
// initialize the form
ugc.init(sessionId);
// attach event handlers to the action buttons
$("#saveButton").click(save);
$("#validateButton").click(validate);
$("#cancelButton").click(cancel)
}
}
The most interesting parts of the code are:
- The call of
ugc.initForm()
: It checks if the form with the id provided in the constructor exists, and only if so, it returns true
.
- The call of
ugc.init(sessionId)
: It initializes the mapping between content and form, registers the wait indicator, if given, and initializes the UGC session. Hence, the form is filled with the content values according to the mappings. Moreover, the optionally given form-init-handler is called.
Defining the mapping between form and content
The mapping of content elements and form fields is declared in the function myMappings
(see the initialization of the UGC object). The function contains just a series of ugc.map(...)
calls:
function myMappings() {
ugc.map("title", "Title[1]", ugc.OPTIONAL);
ugc.map("text", "Paragraph[1]/Text[1]");
ugc.map("imagefile", "Paragraph[1]/Image[1]/Image[1]", ugc.UPLOAD);
/* ... more mappings ... */
}
The map
method takes the name
attribute of the form field and the XPath to the content element it should be connected with. Additional parameters specify the mapping type:
- If no extra parameter is given, the mapping is really one-to-one: The value from the form is always mapped back to the content and the content value into the form.
- If it is
ugc.OPTIONAL
, the content element in the XML content will be removed if the corresponding form field is empty. This mapping type should be used for optional content elements.
- If it is
ugc.UPLOAD
, the mapping will upload a file provided via the form field and link to that file in the content. This type is required for form fields of type <input type="file">
.
Note that you can provide ugc.UPLOAD
and ugc.OPTIONAL
both as parameters.
The mapping is a central element of the UGC object. It is used internally for all communication between form and content. The mapping stores also the values read from the content and has to be updated before you save changes to the content.
Saving values from the form to the content
Saving values is quite simple if no files are included. But, if files are included the upload procedure has to be done before values are saved to the content.
So, if necessary, the first thing to do is to upload files:
ugc.uploadFiles([ "imagefile" ], afterUploadHandler);
The call to ugc.uploadFiles(...)
does the job for you. Provide the list of field names that specifiy the form fields containing the files to be uploaded as the first argument and then a callback function that is called after a successful upload. If upload fails, myErrorHandler
that is provided via the constuctor of the UGC object is called. If upload is successful the afterUploadHandler
is called. It can update the mapping stored by the UGC object, such that it holds the values that should be saved and afterwards write the updated values to the XML content:
function afterUploadHandler(uploadResult) {
ugc.saveContent(updateValues(uploadResult), afterSaveHandler);
}
Saving is managed by ugc.saveContent(...)
. It takes the updated mapping as the first argument and a handler that should be called after successfully storing the XML content. The handler typically destroys the UGC session and replaces the input form by a success notification, or redirects to another web page.
Note that the handler has an argument: uploadResult
. This parameter contains the VFS URIs of the uploaded files given in the order of upload. The URIs must be written to the XML content elements that should link to the uploaded files. Hence, the mapping values must be adjusted. You have to write a function updateValues(...)
that handles these adjustments. Here is an example:
function updateValues(uploadResults) {
ugc.setContent();
var uploads = (typeof uploadResults === "undefined") ? null : uploadResults;
if (uploads != null) {
var image = uploads["imagefile"];
if (image) {
// an image was uploaded
ugc.setContent("imagefile", image);
}
}
return ugc.getContent();
}
In the updateValues(...)
function we first copy the current input form values to the mapping. Secondly, we adjust the values for files that are uploaded and insert the links to the uploaded files. The function could also manipulate the mapping further, e.g., if an optional nested content item has none of its elements non-empty, it can be deleted. To explore and manipulate the mapping, the UGC object provides various methods.
Server-side validation of values
The values in a form may violate validation rules defined for a content item. To check if this is the case, call:
ugc.validate(updateValues(), validateHandler)
Calling ugc.validate(...)
will send the values of the mapping (that we update with the function also used for saving values) given as the first argument to the server and checks if the values can be stored in the XML content. The validationHandler
given as the second argument is called when the validation is performed and it gets a map of validation errors as an argument. A handler implementation could look like:
function validateHandler(errors) {
// form validation handler that displays the results of the server side validation in a simple alert
if ($.isEmptyObject(errors)) {
alert("The values are valid!");
} else {
var out = "The following errors occurred:\n"
for ( var key in errors) {
out += "\n" + key + " : " + errors[key];
}
alert(out);
}
}
Destroying the UGC session
When editing is finished the UGC session must be destroyed. This should happen at the latest when leaving the page. Therefore, add
$(window).unload(function() {
// make sure to kill the UGC session if the page is just left
ugc.destroySession();
});
to your code. The call of ugc.destroySession();
will do the job.
Whenever server-side errors occur, the myErrorHandler
function provided in the constructor of the UGC object is called. A possible implementation is:
function myErrorHandler(errorType, message, additionalData) {
// very simple error handler that just displays an alert box with the raw error text
var out = "Error of type: " + errorType + ": " + message;
if (!$.isEmptyObject(additionalData)) {
out += "\n\nAdditional information:\n";
for ( var key in additionalData) {
out += "\n" + key + ": " + additionalData[key];
}
}
window.alert(out);
}
The UGC object provides various functions to explore and manipulate the mapping between the form fields and the XML content, and also the form. We just list the functions and give a very brief description.
Exploring and manipulating the mapping
- getContent(name?)
Returns the value of the element of the local content specified by name
. If no argument is provided the whole local content is returned.
- setContent(name?)
Reads the content value from the form field with name name
. If name is not provided, it reads the values of the whole form into the local content.
- deleteContent(name)
Delete the content element with the given name
(the field name used for setting up the mapping) when saving changes.
- contentHas(fieldname1,fieldname2,...)
Checks if all content elements assigned to the form fields listed as arguments have a non-empty value.
- contentHasNot(fieldname1,fieldname2,...)
Checks if all content elements assigned to the listed form fields are empty.
- contentHasOne(fieldname1,fieldname2,...)
Checks if at least one of the content elements assigned to the listed form fields is non-empty.
- getForm(name?)
Returns the HTML element of the form's input field specified by name
. If name
is not given, the method returns the HTML element of the whole form.
- getFormVal(name)
Returns the current value of the form's input field specified by name
.
- setForm(name?,value?)
If called without parameters, the method fills all content values into the form. If called with a name
, it fills only the form field name
with the assigned content value. If name
and value
are given, it fills the form field name with value
.
- formHas(fieldname1,fieldname2,...)
Checks if all form fields listed as arguments have a non-empty value.
- formHasNot(fieldname1,fieldname2,...)
Checks if all form fields listed as arguments have an empty value.
- formHasOne(fieldname1,fieldname2,...)
Checks if at least one of the listed form fields is non-empty.
Using the native JavaScript API
In the following, we show several code snippets that explain the JavaScript API. Note, that the code also uses jQuery, but it is not necessary to use it for working with the native JavaScript API.
The frame for your code
$(function() {
var session = null;
var content = null;
var formId = "myFormId";
// function registered as wait callback
// argument: boolean, indicating if waiting for a Server reply
function waitCallback(isWaiting) {
if (isWaiting) {
// …
} else {
// …
}
}
// function registered as client error callback
//argument: string array with stack trace
function clientErrorCallback(error) {
// …
}
// … ALL FUNCTIONS EXPLAINED IN THE FOLLOWING SUBSECTIONS …
window.initUgc = function() {
// set the wait indicator callback
OpenCmsUgc.setWaitIndicatorCallback(waitCallback);
// set the client-side error callback
OpenCmsUgc.setErrorCallback(clientErrorCallback);
// … do whatever else you want, e.g., get a session object:
// Assumption: sessionId is exposed at the <form>-tag as attribute
// “session-id”
var sessionId = $( "#" + formId ).attr("session-id");
getSession(sessionId); // getSession() is a function you have to write
// see the next subsection.
}
$(window).unload(function() {
if (session != null) { session.destroy(); }
});
});
Interesting in the above code fragment is:
- The code is placed in a separate scope (via the surrounding “
$(function () {…});
”). In the scope we have a scope-“global” variable storing the current UGC-session. Moreover, we declare a variable to store the content read from the server and possibly other variables may be useful, such as formId
in the example.
window.initUgc
is a function defined by the UGC JavaScript API and overwritten in the above code. The function is called when the UGC API is completely loaded and ready to use. In this function you should:
- Set a wait indicator handler (optional)
- Set an error handler for client-side errors (optional)
- Get the session object for the current session
Do not use any of the UGC API functions until initUgc
is called! This leads to errors, since the API may not be loaded.
- The established session is destroyed when the page is left (calling
session.destroy()
in (window).unload(…)
).
Getting the session object and the content’s values
// called to get a session for an existing content (specified by the session id)
function getSession(sessionId) {
var elem = document.getElementById( formId );
OpenCmsUgc.initFormForSession(
sessionId // session id used to establish a session for an existing content
// must be provided by the server somehow
, elem // the form used to display/edit the content
// can be null if no files are loaded up
, onNewSession // callback handler on success (session as argument)
, errorHandler // callback handler for server-side errors
);
}
// error handler, used as callback function to display server-side errors
// argument types: String, String, Map
function errorHandler(errorType, message, additionalData) {
// …
}
function onNewSession(s) {
session = s; // store the returned session as scope variable
content = session.getValues(); // get the content values
// … e.g., fill your form, show a picture from the content, …
}
The function getSession()
calls OpenCmsUgc.initFormForSession()
to get a session object. The parameters are explained in the source code.
When a session object is successfully returned, onNewSession(s)
is called with the session object as the argument. We store the object in our scope-“global” variable session and store the content’s values, that are returned by session.getValues()
, in content. The content’s values are given as a map with the XPaths to the content’s elements as keys and the respective values as values. For example, content["Paragraph[1]/Text[1]"]
will yield the content’s value from the node “Paragraph/Text”.
The form can be filled as soon as the content is loaded. Just copy the content values to the form fields, e.g.:
function fillForm() {
if (content != null) {
$("input[name='titlefield']").val(content["Title[1]"]);
$("input[name='textfield']").val(content["Paragraph[1]/Text[1]"]);
// …
}
}
Of course, more complex mappings of values are possible.
Before uploading the form values to store them in the content, they have to be mapped to a map that assigns XPaths to values, like the map in which content was returned from the server.
function updateValues() {
var result = {} ;
result["Title[1]"] = $("input[name='titlefield']").val();
result["Paragraph[1]/Text[1]"] = $("input[name='textfield']").val();
// …
return result;
}
Note that there is a special treatment for file uploads. There you can’t just copy the form’s input to the content. You must upload the file before and then get back the link to the file from the server. See below for this.
Note also: To delete an optional node from a content item, you must set its value explicitly to null. If the node is just not part of the result map, it remains unchanged. Thus to delete a node, add
result["XPath[1]/To[1]/Node[1]"] = null;
to the result map. To leave a node unchanged, just do not add an entry for the node’s XPath to result
.
Having updated values, you can validate or save them:
function validate() {
var newValues = updateValues();
session.validate(newValues, validationReplyHandler)
}
function validationReplyHandler(errors) {
// handle the errors that are provided as
// map “XPath of the element (String) -> error message (String)”
}
function save() {
session.uploadFiles(["uploadfield"], saveContent, errorHandler);
}
function saveContent(uploadResult) {
var newValues = updateValues();
var pic = uploadResult["uploadfield"];
if (pic) { // file for upload is given
newValues["Paragraph[1]/Image[1]/Image[1]"] = pic;
}
session.saveContent(
newValues
, function(s) {
// handler called on successful upload, s is the empty string
}
, errorHandler
);
}
To validate your values against conditions defined in the XSD of the edited content’s type, call session.validate(values, replyHandler)
. After validation, the reply-handler is called and fed with a map containing the errors that occurred during the validation.
Saving content is very easy when no file upload is included: just call session.saveContent(values, successHandler, errorHandler)
. If files need to be uploaded, then first call session.uploadFiles(filefieldlist, successHandler, errorHandler)
and update the content only on success. How to use this function should be clear from the above example.
Additional useful functions
The session object provides a few more helpful functions.
Convert a VFS URI to a valid link
session.getLink(OpenCmsVfsLink, function(realLink) {/* use realLink */});
The getLink()
function takes a OpenCms VFS URI, e.g., the link to a picture as stored in a content item, and converts it into the URI that is used to access the linked resource from the browser. The generated link is given as the argument to the callback function.
Get the VFS URI of the edited content
var contentVfsUri = session.getSitePath();
The getLink()
function takes a OpenCms VFS URI, e.g., the link to a picture as stored in a content item, and converts it into the URI that is used to access the linked resource from the browser. The generated link is given as the argument to the callback function.
Get the session-id
var sessionId = session.getSessionId();
Some implementation details
Actions performed via UGC are always performed in sessions. To add or edit content via UGC, a new session is created. This means a new project is created in OpenCms, and a new session object is created. The session object is exposed to the client via a session-id written somewhere in the HTML code. The id is used by the UGC JavaScript-API to get the session object. This object provides facilities to access and edit XML content in the newly created project.
A project created for a session is removed when it is published. This can, depending on the configuration, either happen when the session object is destroyed or when a user with the according rights publishes the project.
Note that UGC sessions are created on the server-side via a special JSP-Tag. Server-side code for session initialization is a matter of security: full control over which user can edit/add which content in which situation is preserved on the server-side.
More information on User-Generated Content is found in
- OpenCms JavaDoc: Look for the package
org.opencms.ugc.client.export
to get detailed information on the JavaScript-API
opencms.tld
(and the according documentation): here you'll find information about the tag <cms:ugc>
- OpenCms Demo Website: watch out for an example for User-Generated Content.