invalid command name "ns_adp_include"
while executing
"ns_adp_include header.adp "
invoked from within chunk: 2 of adp: /home/dvr/web/dvr/cmd/custom-handlers.adp
A well-designed CMS will have two distinct parts: (1) a set of core services that support all types of content, and (2) small pieces of custom code that plug into the core and support specific content types. The code that translates a url to a revision_id is in the first type; the code that fetches the content fields for a news story and formats them in a way specific for news stories is the second. Adding a new content type shouldn't require modifications to the core tables or plsql.
Beyond the extension tables that hold a type's custom fields, adding a new type requires 6 small pieces of code:
These are called "handlers" because they plug into the rest of the system and handle specific tasks for their content type. If the CMS receives a request for /publish/foo/bar and determines this is the url for a story, it will first verify the user has access to view the story (an operation that's the same for all content items), then hand off control to a custom handler that fetches/formats the content and writes it out to the user.
A developer writing a custom handler doesn't have the burden of implementing the system's rules. For example, the handler that updates the content type's custom fields doesn't need to verify the user has the item checked out; the handler will be called only after the CMS has verified it's the right user. The handler also won't need to update the revision's audit fields because these fields will have already been updated. Similarly, the handler to generate the edit form will only be called if the user has the item checked out, and the "public view" handler will only be called if this user has permission to view the story.
The first four handlers will be written in our scripting language. Here is a summary of each:
These four handlers will be grouped together in some way that makes sense for the language being used. In Java, they'll be four static methods in the same class (and the name of the class will be stored in cm_types). If a scripting language like php is used, the easiest solution is to follow a naming convention that uses the type's type_key in the function name -- for example, the handler to update a story's type (type_key = 'story') would be cm_update_story.
The last two handlers need to be written in plsql because they will be called from within the core plsql:
#1: The "public view" handler
All public content will be served from beneath /publish/. If the editor chooses /bush/interview.html as the url, the public url will be /publish/bush/interview.html. Staff members will be able to preview the most recent version at /preview/bush/interview.html.
All requests to /publish* and /preview* will be handled by cm_php cm_public_url_handler, which looks at the url to choose the correct revision, determines the class for that of content, then calls the type's custom handler.
function cm_public_url_handler () {
$full_url = $_SERVER['REQUEST_URI'];
// If the requested url is /live/foo/bar, we want to split it
// to 'live' and /foo/bar'
if (!preg_match("/^\/(live|preview)(\/.*)/", $full_url, $url_parts)) {
cm_die_not_found();
} else {
$mode = $url_parts[1];
$url = $url_parts[2];
$bind_vars = array();
if ($mode == 'preview') {
cm_in_preview_mode_p(1);
// Only staff members are allowed to use the preview
// mode. If this person isn't on staff, don't let them go
// any further.
cm_restrict_to_staff();
// Staff members are allowed to preview any revision.
// By default, they preview the last revision, but they
// can specify a revision_id.
$chosen_revision_id = form_value('revision_id');
if ($chosen_revision_id != '') {
// They're choosing the revision
$content_view = 'cm_v_content';
$where_clause = "revision_id = :revision_id";
$bind_vars['revision_id'] = $chosen_revision_id;
} else {
// Choose the last revision
$content_view = 'cm_vlast_content';
$where_clause = "item_id = (select cm_item_for_url(:url) from dual)";
$bind_vars['url'] = $url;
}
} else {
// They're asking for the live revision
$content_view = 'cm_vlive_content';
$where_clause = "item_id = (select cm_item_for_url(:url) from dual)";
$bind_vars['url'] = $url;
}
$db = db_open();
$q = db_prepare($db, "
select
type_key,
directory_id,
revision_id
from $content_view
where $where_clause");
foreach($bind_vars as $bind_name => $bind_value) {
db_bind($q, $bind_name, $bind_value);
}
db_execute($q);
if (db_getrow($q)) {
$type_key = db_getcol($q, 'type_key');
$revision_id = db_getcol($q, 'revision_id');
cm_page_info(directory_id, db_getcol($q, 'directory_id'));
db_close($db);
// Figure out the name of the content type's handler
$handler_name = "cm_write_public_$type_key";
// Call the handler
$handler_name($revision_id);
} else {
// a revision doesn't exist
db_close($db);
cm_die_not_found();
}
}
}
If the item is a story, the result will be a call to cm_write_public_story, which fetches the correct fields, formats them, and writes them to the user:
function cm_write_public_story ($revision_id) {
$db = db_open();
$q = db_prepare($db, "
select
s.headline,
s.subhead,
s.body,
a.template_filename
from cmx_r_story s, cm_v_templates_assigned a
where s.revision_id = :revision_id
and s.revision_id = a.revision_id(+)");
db_bind($q, revision_id, $revision_id);
db_execute($q);
db_getrow($q);
foreach(db_colnames($q) as $colname) {
$$colname = db_getcol($q, $colname);
}
$body = cm_process_tags_in_text($body);
cm_page_info("page_title", $headline);
db_close($db);
header('Content-Type: text/html');
if ($template_filename)
include(cm_template_dir() . '/' . $template_filename);
else
echo '(no template)';
}
#2 & #3: The "staff view" and "edit form" handlers
A staff member looking at an item on the staff pages will see a form to edit a item if (1) it's the last revision and (2) the item is checked out to them; otherwise they'll see the staff view of the revision. The admin page will print the header fields common to all content, then call one of the type's custom handlers.
If the system calls the "edit form" handler, the user will see a page like:
![]() |
| The "edit form" handler generates the light gray part of the page. The fields in the dark gray are common to all content types, so no new code needs to be written for that part of the form. |
The source for this handler:
function cm_edit_form_story ($revision_id) {
$db = db_open();
$q = db_prepare($db, "
select
headline,
subhead,
body
from cmx_r_story
where revision_id = :revision_id");
db_bind($q, revision_id, $revision_id);
db_execute($q);
db_getrow($q);
foreach (db_colnames($q) as $colname) {
$$colname = db_getcol($q, $colname);
}
// Generate the form value html, i.e., VALUE="..."'
$fv_headline = form_value_html_for_value(db_getcol($q, headline));
// Generate the html that goes between the TEXTAREA tags.
$ta_subhead = form_textarea_value_html_for_value(db_getcol($q, subhead));
$ta_body = form_textarea_value_html_for_value(db_getcol($q, body));
db_close($db);
return "
<table cellspacing=5>
<tr>
<th align=left>Headline</th>
<td><input name=headline size=50 $fv_headline></td>
</tr>
<tr valign=top>
<th align=left>Subhead</th>
<td><textarea name=subhead cols=40 rows=2>$ta_subhead</textarea></td>
</tr>
<tr>
<td colspan=2>
<b>Body</b><br>
<textarea name=body cols=50 rows=20>$ta_body</textarea>
</td>
</tr>
</table>";
}
If the user doesn't have it checked out, or it isn't the last revision, they see the "admin view":
![]() |
| The "admin view" handler generates the light gray part of the page. |
The source:
function cm_admin_view_story ($revision_id) {
$db = db_open();
$q = db_prepare($db, "
select
headline,
subhead,
body
from cmx_r_story
where revision_id = :revision_id");
db_bind($q, revision_id, $revision_id);
db_execute($q);
db_getrow($q);
$headline = db_getcol($q, headline);
$subhead = db_getcol($q, subhead);
// We want to show the html source for the body, so escape it and
// word-wrap it.
$body_fm = wordwrap(htmlspecialchars(db_getcol($q, body)), 60);
db_close($db);
return "
<table cellpadding=5>
<tr>
<th align=left>Headline</th>
<td>$headline</td>
</tr>
<tr>
<th align=left>Subhead</th>
<td>$subhead</td>
</tr>
<tr valign=top>
<th align=left>Body</th>
<td><pre>$body_fm</pre></td>
</tr>
</table>";
}
#4: The update handler
The update handler receives values from the "edit form" handler and writes them to the database.
For the story type, the update handler is cm_update_story
function cm_update_story ($revision_id) {
$db = db_open();
$q = db_prepare($db, "
update cmx_r_story set
headline = :headline,
subhead = :subhead,
body = :body
where revision_id = :revision_id");
db_bind($q, revision_id, $revision_id);
db_bind($q, headline, form_value(headline));
db_bind($q, subhead, form_value(subhead));
db_bind($q, body, form_value(body));
db_execute($q);
db_close($db);
}
This handler doesn't need to update audit fields or verify that the user is allowed to update this revision because the handler is called within a block of code like:
db_transaction_begin($db);
// Try to update the audit fields. Throw an exception if the
// user isn't allowed.
$q = db_prepare($db, "
begin
cm_revision_update (
p_revision_id => :revision_id,
p_user_id => :caller_id
);
end;");
db_bind($q, revision_id, $revision_id);
db_bind($q, caller_id, $caller_id);
// This executes the plsql block above and checks for any
// errors returned. If it determines the error is a message from
// the core, it displays the error to the user.
cm_core_db_execute($q);
// Choose the handler to call
$update_handler_name = "cm_update_" . $type_key;
// Call the handler
$update_handler_name($revision_id);
// If the call to the handler completes without error, commit
// everything.
db_transaction_end($db);
cm_revision_update verifies the user has the item checked out and it's the item's last revision being updated. If either check fails, an exception is throw and the handler is never called. If both checks pass, the audit fields are updated and the handler runs. If the handler dies with an error, the update to the audit fields will be rolled back because the call to cm_transaction_end that commits the update will never be called.
#5 & #6: The "create item" and "create revision" handlers
The last two handlers are written in plsql because they need to be called from within the core plsql. This ensures that the inserts into the extension tables are done in the same transaction as the inserts into cm_items and cm_revisions, and if any of these inserts fail for some reason, we're not left with a half-entered piece of content (i.e., a revision with audit information, but no content).
The "create revision" handler is called from within cm_revision_create:
create or replace function cm_revision_create (
--
-- Create a new revision based on the previous revision for
-- this item.
--
p_item_id in integer,
p_user_id in integer,
p_state_key in cm_workflow_states.state_key%type
) return integer
as
v_revision_id integer;
v_revision_to_copy integer;
v_type_key cm_types.type_key%type;
begin
select revision_id, type_key
into v_revision_to_copy, v_type_key
from cm_vlast_content
where item_id = p_item_id;
select cm_sequence.nextval into v_revision_id from dual;
insert into cm_revisions (
revision_id, item_id, publication_date, template_id,
audit_user, audit_creation_date,
state_key, state_audit_user, state_audit_date)
select
v_revision_id, p_item_id, publication_date, template_id,
p_user_id, sysdate,
p_state_key, p_user_id, sysdate
from cm_revisions
where revision_id = v_revision_to_copy;
execute immediate '
begin
cm_revision_create_' || v_type_key || '(:1, :2);
end;'
using v_revision_id, v_revision_to_copy;
return v_revision_id;
end;
The 'execute immediate' block is what calls the custom handler. Since the name of the handler is determined at run-time, we build up the plsql block like a string, then evaluate the string by feeding it to 'execute immediate'. In the case of a story, we're calling cm_revision_create_story.
The "create revision" handler takes two arguments: the revision_id of the new revision, and the revision_id of the revision whose values should be copied to populate the new revision. The source for cm_revision_create_story is:
create or replace procedure cm_revision_create_story (
p_revision_id in integer,
p_revision_to_copy in integer
)
as
begin
insert into cmx_r_story (revision_id, headline, subhead, body)
select p_revision_id, headline, subhead, body
from cmx_r_story
where revision_id = p_revision_to_copy;
end;
The second handler, cm_item_create_type_key is called when an item is created:
create or replace procedure cm_item_create_story (
p_item_id in integer,
p_revision_id in integer
)
as
begin
insert into cmx_r_story (revision_id) values (p_revision_id);
end;
which is called from within cm_item_create:
create or replace function cm_item_create (
--
-- Create a new content item and return the revision_id
--
p_item_id in integer default null,
p_type_key in cm_types.type_key%type,
p_department_id in integer,
p_short_name in cm_items.short_name%type default null,
p_state_key in cm_workflow_states.state_key%type default 'created',
p_user_id in integer
) return integer
as
v_item_id integer := p_item_id;
v_revision_id integer;
v_name_taken_p integer;
v_type_key cm_types.type_key%type;
begin
cm_verify_not_null(p_department_id, 'A department is required');
cm_department_verify_allowed (
p_department_id => p_department_id,
p_user_id => p_user_id,
p_action => 'add_item',
p_error => 'You''re not allowed to add an item to this department'
);
if v_item_id is null then
select cm_sequence.nextval into v_item_id from dual;
end if;
if p_short_name is not null then
select count(*) into v_name_taken_p
from cm_items
where short_name = p_short_name;
if v_name_taken_p = 1 then
cm_throw_exception('That name is already taken');
end if;
end if;
insert into cm_items (
item_id, type_key, department_id, short_name, audit_user
) values (
v_item_id, p_type_key, p_department_id, p_short_name, p_user_id
);
select cm_sequence.nextval into v_revision_id from dual;
insert into cm_revisions (
revision_id, item_id, audit_user, audit_creation_date,
state_key, state_audit_user, state_audit_date
) values (
v_revision_id, v_item_id, p_user_id, sysdate,
p_state_key, p_user_id, sysdate
);
cm_verify_state_allowed (
p_revision_id => v_revision_id,
p_user_id => p_user_id,
p_state_key => p_state_key
);
execute immediate '
begin
cm_item_create_' || p_type_key || '(:1, :2);
end;'
using v_item_id, v_revision_id;
return v_revision_id;
end cm_item_create;
invalid command name "ns_adp_include"
while executing
"ns_adp_include footer.adp "
invoked from within chunk: 12 of adp: /home/dvr/web/dvr/cmd/custom-handlers.adp