'image_button',
'#title' => t('My button'),
'#return_value' => 'my_data',
'#src' => 'my/image/path.jpg',
); */
/* TODO Remove $row argument from db_result() method
The $row argument of db_result() was removed from the database abstraction
layer in 6.x core, as it was a database dependent option. Developers need to
use other handling to replace the needs of this method. */
/* TODO Remove db_num_rows() method
The db_num_rows() method was removed from the database abstraction layer in
6.x core, as it was a database dependent method. Developers need to use other
handling to replace the needs of this method. */
/* TODO Change 'Submit' to 'Save' on buttons
It has been agreed on that the description 'Submit' for a button is not a
good choice since it does not indicate what actually happens. While for
example on node editing forms, 'Preview' and 'Delete' describe exactly what
will happen when the user clicks on the button, 'Submit' only gives a vague
idea. When labelling your buttons, make sure that it is clear what this
button does when the user clicks on it. */
/* TODO Node previews and adding form fields to the node form.
There is a subtle but important difference in the way node previews (and other
such operations) are carried out when adding or editing a node. With the new
Forms API, the node form is handled as a multi-step form. When the node form
is previewed, all the form values are submitted, and the form is rebuilt with
those form values put into $form['#node']. Thus, form elements that are added
to the node form will lose any user input unless they set their '#default_value'
elements using this embedded node object. */
/* TODO New user_mail_tokens() method may be useful.
user.module now provides a user_mail_tokens() function to return an array
of the tokens available for the email notification messages it sends when
accounts are created, activated, blocked, etc. Contributed modules that
wish to make use of the same tokens for their own needs are encouraged
to use this function. */
/* TODO
There is a new hook_watchdog in core. This means that contributed modules
can implement hook_watchdog to log Drupal events to custom destinations.
Two core modules are included, dblog.module (formerly known as watchdog.module),
and syslog.module. Other modules in contrib include an emaillog.module,
included in the logging_alerts module. See syslog or emaillog for an
example on how to implement hook_watchdog.
function example_watchdog($log = array()) {
if ($log['severity'] == WATCHDOG_ALERT) {
mysms_send($log['user']->uid,
$log['type'],
$log['message'],
$log['variables'],
$log['severity'],
$log['referer'],
$log['ip'],
format_date($log['timestamp']));
}
} */
/* TODO Implement the hook_theme registry. Combine all theme registry entries
into one hook_theme function in each corresponding module file.
function netforum_node_theme() {
return array(
'netforum_node_object_lookup' => array(
'file' => 'netforum_node.module',
'arguments' => array(
'obj_type_name' => '',
),
),
);
} */
/* TODO Form buttons can define custom #submit and #validate handlers.
All forms can have #validate and #submit properties containing lists of
validation and submission handlers to be executed when a user submits data.
Previously, if a form featured multiple submission buttons to initiate
different actions (updating a record versus deleting, for example), it was
necessary to check the incoming form_values['op'] for the name of the
clicked button, then execute different code based on its value. Now, it is
possible to define #validate and #submit properties on each individual form
button if desired. */
//IDEA: multiple node templates
//IDEA: Popup reference/help for lists and queries in node bodies
//TODO: Test node operations
/**
* @file
* The netFORUM Dynamic Facade Nodes module
*
* Uses the netFORUM xWeb module to fetch Objects from netFORUM and
* turn them into Nodes for viewing. The nodes work as a cross between
* netFORUM e-marketing (correspondence) templates and the List content type in eWeb.
* Most e-marketing templates should look the same as nodes, but child_form_html and
* child_form_text commands are ignored. Much of the functionality can be reproduced using
* the list functionality.
*
* Many functions and behavior of this module is patterend, copied, or adapted from the Node module
*
*/
/**
* Implementation of hook_menu()
*/
function netforum_node_menu() {
$items = array();
//'create netFORUM node', 'edit own netFORUM nodes', 'edit netFORUM nodes', 'administer netFORUM node templates', 'administer netFORUM node special fields'
global $user;
/*$node_access_perm = (user_access('administer site configuration') || user_access('create netFORUM node') || user_access('edit netFORUM nodes') || (user_access('edit own netFORUM nodes') && ($user->uid == $node->uid)) );
$template_access_perm = (user_access('administer site configuration') || user_access('administer netFORUM node templates'));
$special_field_access_perm = (user_access('administer site configuration') || user_access('administer netFORUM node special fields'));
*/
/* TODO
Non menu code that was placed in hook_menu under the '!$may_cache' block
so that it could be run during initialization, should now be moved to hook_init.
Previously we called hook_init twice, once early in the bootstrap process, second
just after the bootstrap has finished. The first instance is now called boot
instead of init.
In Drupal 6, there are now two hooks that can be used by modules to execute code
at the beginning of a page request. hook_boot() replaces hook_boot() in Drupal 5
and runs on each page request, even for cached pages. hook_boot() now only runs
for non-cached pages and thus can be used for code that was previously placed in
hook_menu() with $may_cache = FALSE:
Dynamic menu items under a '!$may_cache' block can often be simplified
to remove references to arg(n) and use of '% '. t('This action cannot be undone.') .' '. t('This action cannot be undone.') .'
This module adds a new node type that can fetch information from netFORUM and format it. The node title and body work much like e-Marketing (correspondence) templates in netFORUM, using curly brackets to insert data.
Most object fields are available, but newly added fields may not show up until you !refresh_obj_fields. The {child_form_text} and {child_form_html} statements are ignored.
Conditional statements are fully supported, here are some examples:
". t("More than @numrows matches found, try being more specific.", array('@numrows' => $max_rows_return)) ."
";
}
$output .= $search_results;
$output .= drupal_render($form);
if ($exit_after) {
print $output;
//If we got no response, it might have set an error using drupal_message. Since we could be ajaxin' it up here,
//clear out those errors.
drupal_get_messages();
exit;
}
else {
return $output;
}
}
/**
* Implementation of node_insert()
*/
function netforum_node_insert($node) {
db_query("INSERT INTO {netforum_node} (nid, vid, nfn_obj_key, nfn_key, unparsed_title, unparsed_body) VALUES(%d, %d, '%s', '%s', '%s', '%s')",
$node->nid, $node->vid, netforum_object_key($node->obj_type_name), $node->nfn_key, $node->unparsed_title, $node->unparsed_body);
}
/**
* Implementation of node_update()
*/
function netforum_node_update($node) {
if ($node->revision) {
netforum_node_insert($node);
}
else{
db_query("UPDATE {netforum_node} set nfn_obj_key = '%s', nfn_key = '%s', unparsed_title = '%s', unparsed_body = '%s' WHERE nid='%d' AND vid='%d' ",
netforum_object_key($node->obj_type_name), $node->nfn_key, $node->unparsed_title, $node->unparsed_body, $node->nid, $node->vid);
}
}
/**
* Implementation of node_submit()
*/
function netforum_node_submit($form, &$form_state) {
netforum_node_set_to_template($node); //This will only override our input if we pass TRUE as the second parameter, so we're safe
netforum_node_refresh_fields($node);
}
/**
* Implementation of node_load()
*/
function netforum_node_load($node) {
$op = isset($_POST['op']) ? $_POST['op'] : '';
if ($op == 'Preview') {
//this data is going to get used to generate a preview, so we ought ot set it...
$node_part = new stdClass();
$node_part->nfn_key = $_POST['nfn_key'];
$node_part->obj_type_name = trim($_POST['obj_type_name']);
$node_part->nfn_obj_key = netforum_object_key($node_part->obj_type_name);
$node_part->unparsed_body = $_POST['unparsed_body'];
$node_part->unparsed_title = $_POST['unparsed_title'];
netforum_node_set_to_template($node_part);
}
else{
$res = db_query("SELECT nfn_obj_key, a.nfn_key as nfn_key, obj_name as obj_type_name, unparsed_title, unparsed_body FROM
{netforum_node} a
JOIN {netforum_object_cache} b ON a.nfn_obj_key = b.obj_key
WHERE nid=%d
AND vid=%d", $node->nid, $node->vid);
$node_part = db_fetch_object($res);
}
return $node_part;
}
/**
* Implementation of node_delete()
*/
function netforum_node_delete($node) {
db_query("DELETE FROM {netforum_node} WHERE nid=%d", $node->nid);
}
/**
* Implementation of node_view()
*/
function netforum_node_view($node, $teaser = FALSE, $page = FALSE) {
$op = isset($_POST['op']) ? $_POST['op'] : '';
if ($op == 'Preview') {
netforum_node_set_to_template($node);
}
$obj_key = netforum_is_valid_guid($node->nfn_obj_key) ? $node->nfn_obj_key : netforum_object_key($node->obj_type_name);
if (! isset($node->nfn_data) || empty($node->nfn_data)) {
$page_title = drupal_get_title();
$old_node_title = $node->title;
netforum_node_refresh_fields($node);
netforum_node_save_fields($node);
//Update the title of the page if it changed
if ($page_title == $old_node_title && $node->title != $old_node_title) {
drupal_set_title($node->title);
}
}
if (!$teaser) {
//Mostly this is a sanity check, but if they are editing or previewing a node it will be handy
if ($node->body == $node->unparsed_body || empty($node->body)) {
$node->body = netforum_node_process_text($node->unparsed_body, $node->nfn_data, $obj_key);
}
if ($node->title == $node->unparsed_title || empty($node->title) ) {
$node->title = netforum_node_process_text($node->unparsed_title, $node->nfn_data);
}
$node = node_prepare($node, $teaser);
}
if ($teaser) {
$node->teaser = netforum_node_process_text($node->teaser, $node->nfn_data, $obj_key);
$node = node_prepare($node, $teaser);
}
return $node;
}
/**
* Create a new node programatically from a template
*
* The node's body and title are set to the default template and inserted in the databse
*
* @param $nfn_obj_key
* A string representing the netFORUM key of the node object type
* @param $nfn_key
* A string representing the unique key of the netFORUM object
*/
function netforum_node_create_from_template($nfn_obj_key, $nfn_key) {
$node = new stdClass();
$node->type = 'netforum_node';
$node->log = t('Created on demand by a {BeginLink}{EndLink} tag');
$node->nfn_key = $nfn_key;
$node->nfn_obj_key = $nfn_obj_key;
$node->obj_type_name = netforum_object_name($nfn_obj_key);
$node_options = variable_get('node_options_'. $node->type, array('status'));
foreach (array('status', 'promote', 'sticky', 'revision') as $key) {
$node->$key = in_array($key, $node_options);
}
if (false) {
drupal_goto($_GET['destination']);
}
netforum_node_set_to_template($node);
if (isset($node->unparsed_body) == false || isset($node->unparsed_title) == false) {
drupal_not_found();
return;
}
netforum_node_refresh_fields($node);
if (isset($node->nfn_data) == false) {
drupal_not_found();
return;
}
node_save($node);
drupal_goto('node/'. $node->nid);
}
/**
* Set the nodes body and title to the netFORUM node template
*
* @param &$node
* A netFORUM node object with either an object key or name set
* @param $overwrite_fields
* Boolean, defaults to false. If set to true the body and title will be overwritten with
* the template
*/
function netforum_node_set_to_template(&$node, $overwrite_fields = false) {
if ($overwrite_fields != true && trim($node->unparsed_body) != '' && trim($node->unparsed_title) != '') {
//no overwrite needed because the fields have a value and we weren't instructed to overwrite them
return;
}
$obj_key = netforum_is_valid_guid($node->nfn_obj_key) ? $node->nfn_obj_key : netforum_object_key($node->obj_type_name);
if ( netforum_is_valid_guid($obj_key)) {
$template = db_fetch_array(db_query("SELECT default_body, default_title FROM {netforum_node_types} WHERE nft_obj_key = '%s' ", $obj_key));
if ($template) {
if ($overwrite_fields || trim($node->unparsed_body) == '') {
$node->unparsed_body = $template['default_body'];
}
if ($overwrite_fields || trim($node->unparsed_title) == '') {
$node->unparsed_title = $template['default_title'];
}
}
}
}
/**
* Fetch new data for the netFORUM node and update the body and title
*
* $node->nfn_skip_save is set by this function. If the data came from the cache
* or doesn't change the body or title nfn_skip_save is set to true
*
* @param &$node
* A netFORUM node object
* @param $ignore_cache
* Boolean, defaults to false. If set to true the latest data will be fetched from
* netFORUM and not from the local DB cache
*/
function netforum_node_refresh_fields(&$node, $ignore_cache = false) {
$save_not_needed = false;
netforum_node_refresh_data($node, $ignore_cache);
if (isset($node->nfn_data)) {
$obj_key = netforum_is_valid_guid($node->nfn_obj_key) ? $node->nfn_obj_key : netforum_object_key($node->obj_type_name);
$new_body = netforum_node_process_text($node->unparsed_body, $node->nfn_data, $obj_key);
$new_title = netforum_node_process_text($node->unparsed_title, $node->nfn_data);
$save_not_needed = (strcmp($new_body, $node->body) == 0 && strcmp($new_title, $node->title) == 0);
$node->body = $new_body;
$node->title = $new_title;
$node->teaser = isset($node->body) ? node_teaser($node->body, isset($node->format) ? $node->format : NULL) : '';
}
else{
$save_not_needed = true;
}
$node->nfn_skip_save = $save_not_needed;
}
/**
* Get the latest data for a node from netFORUM
*
* @param &node
* The netFORUM Dynamic Facade Node with an object key and object type key set
* @return
* Nothing, the node is passed by reference
*/
function netforum_node_refresh_data(&$node, $ignore_cache = false) {
if ( (isset($node->nfn_data) && $ignore_cache == false) ) {
return;
}
$obj_name = $node->obj_type_name;
if (!isset($obj_name) && netforum_is_valid_guid($node->nfn_obj_key) ) {
$obj_name = netforum_object_name($node->nfn_obj_key);
}
$nfn_key = $node->nfn_key;
if ( isset($obj_name) == false || netforum_is_valid_guid($nfn_key == false)) {
return;
}
if ($ignore_cache) {
$cache_time = '1 second';
}
else{
$cache_time = variable_get('netforum_cache_default', '1 hour');
// only look in the default cache, if we're refreshing the information we don't want really old (possibly bad) data
}
$results = netforum_xweb_request('GetFacadeObject', array('szObjectName' => $obj_name, 'szObjectKey' => $nfn_key), $cache_time);
if (is_null($results)) {
watchdog('netforum', 'No data for Dynamic Facade Node type !obj_name found for key !key', array('!obj_name' => $obj_name, '!key' => $nfn_key), WATCHDOG_ERROR, l(t('node'), 'node/'. $node->nid));
}
else {
$node->nfn_data_from_cache = netforum_response_from_cache();
$node->nfn_data = $results;
}
}
/**
* Implementation of hook_perm()
*/
function netforum_node_perm() {
return array('create netFORUM node', 'edit own netFORUM nodes', 'edit netFORUM nodes', 'administer netFORUM node templates', 'administer netFORUM node special fields');
}
/**
* Save the node body and title to the database unless $node->nfn_skip_save is true
*
* This is used to make sure that the content in the database is always the most current, which is important
* in the event that the netFORUM server is unreachable
*
* @param $node
* A netFORUM node object
*/
function netforum_node_save_fields($node) {
if ($node->nfn_skip_save == true || !isset($node->nid) || !isset($node->vid) ) {
return;
}
db_query("UPDATE {node_revisions} SET title='%s', body='%s', teaser='%s' WHERE nid='%d' AND vid='%d'",
$node->title, $node->body, $node->teaser, $node->nid, $node->vid);
db_query("UPDATE {node} SET title='%s' WHERE nid='%d' AND vid='%d'", $node->title, $node->nid, $node->vid);
}
/**
* Implementation of hook_access()
*/
function netforum_node_access($op, $node, $account) {
if ($op == 'create') {
return (user_access('create netFORUM node', $account));
}
if ($op == 'update' || $op == 'delete') {
return ((user_access('edit own netFORUM nodes', $account) && ($account->uid == $node->uid)) || user_access('edit netFORUM nodes', $account));
}
}
/**
* Implementation of hook_node_operations()
*/
function netforum_node_node_operations() {
$operations = array(
'netforum_node_refresh_data' => array(
'label' => t('Refresh data from netFORUM'),
'callback' => 'netforum_node_operations_refresh',
),
'netforum_node_template' => array(
'label' => t('Reset to netFORUM object template'),
'callback' => 'neforum_node_operations_set_template',
),
);
return $operations;
}
/**
* Node operation to reset the nodes to the default template
*
*/
function neforum_node_operations_set_template(&$form_state, $nodes) {
$templates = array();
$res = db_query("SELECT nft_obj_key, default_title, default_body FROM {netforum_node_types} ");
while ($node_template = db_fetch_array($res)) {
$templates[$node_template['nft_obj_key']] = $node_template;
}
foreach ($nodes as $nid) {
$node = node_load($nid);
if ($node->type == 'netforum_node') {
if ( array_key_exists($node->nfn_obj_key, $templates)) {
$node->revision = 1;
$node->unparsed_title = $templates[$node->nfn_obj_key]['default_title'];
$node->unparsed_body = $templates[$node->nfn_obj_key]['default_body'];
$node->log = t("Node reset to template default during bulk operation");
netforum_node_refresh_fields($node);
//work around for the weird pathauto bug that expects taxonomy to be an array and not an object, like it was coming from a form.
// For the record, grr.
if ( module_exists('pathauto') ){
$new_tax = array();
if ($node->taxonomy) {
$vocabularies = taxonomy_get_vocabularies();
foreach ($node->taxonomy as $tid => $value) {
if (is_object($value)) {
if ($vocabularies[$value->vid]->tags) {
if (array_key_exists('tags', $new_tax) == false) {
$new_tax['tags'] = array();
$new_tax['tags'][$value->vid] = array();
}
$new_tax['tags'][$value->vid][] = $value->name;
}
else {
if (array_key_exists($value->vid, $new_tax) == false) {
$new_tax[$value->vid] = array();
}
$new_tax[$value->vid][$value->tid] = $value->tid;
$node->taxonomy[$tid] = (array)$value;
}
}
}
if (count($new_tax) > 0) {// at the end, we should have an array that looks like it came from a form. There is a much simpler
//patch for this, but I'd rather implement an ugly workaround than make people patch other modules' code
if (array_key_exists('tags', $new_tax)) {
foreach ($new_tax['tags'] as $vid => $tags) {
$new_tax['tags'][$vid] = implode(', ', $tags);
}
}
$node->taxonomy = $new_tax;
}
}
}
node_save($node);
}
}
}
}
/**
* Node operation to fetch recent data from netFORUM
*
*/
function netforum_node_operations_refresh(&$form_state, $nodes) {
foreach ($nodes as $nid) {
$node = db_fetch_object(db_query("SELECT obj_name as obj_type_name, nfn_key, unparsed_body, unparsed_title, a.nid, n.vid FROM {netforum_node} a
JOIN {netforum_object_cache} b ON a.nfn_obj_key = b.obj_key
JOIN {node} n ON n.nid = a.nid AND n.vid = a.vid
WHERE a.nid= %d", $nid));
if ($node) {
netforum_node_refresh_fields($node, true);
netforum_node_save_fields($node);
}
}
}
/**
* Implementation of hook_node_info()
*/
function netforum_node_node_info() {
return array(
'netforum_node' => array(
'name' => t('netFORUM Dynamic Facade Node'),
'module' => 'netforum_node',
'description' => t('Convert a netFORUM Object to a Drupal node'),
'has_title' => TRUE,
'title_label' => t('Title'),
'has_body' => TRUE,
'body_label' => t('Body'),
'locked' => TRUE,
),
);
}
/**
* Interpret special directives inside the the text and produce a final output.
*
* Dispatch the text for processing by various functions depending on the directives in the text.
* This should be safe to use on most text to produce an output suitable for viewing or processing
* by a text filter
*
* @param $text
* A string with the plaint text to be replaced
* @param $object_data
* A hashed array or object representing an object with keys set to column names and values set to the column data.
* @param $obj_type_key
* The object key representing the object that the data came from, if this is set then {BeginLink} and {EndLink}
* will be enabled.
* @return
* A string with the parsed text
* @see netforum_node_process_list_tags()
* @see netforum_node_process_link_tags()
* @see netforum_node_process_curly_tags()
*/
function netforum_node_process_text($text, $object_data = array(), $obj_type_key = '') {
if ($text == '' || stristr($text, "{") == false ) {
return $text;
}
if (is_array($object_data) == false) {
$object_data = get_object_vars($object_data);
}
if (is_array($object_data) == false || count($object_data) == 0) {
return $text;
}
$special_curlies = netforum_node_special_fields();
foreach (array_keys($object_data) as $col) {
if (stristr($col, "html")) { //look for columns with HTML in the name and parse them for curly brackets just in case there are any calls to {ApplicationPath} or similar
if ($ignore_html_tag) {
$object_data[$col] = '';
}
else{
//This way the final output has the parsed html, not the straight database values
$object_data[$col] = netforum_node_process_curly_tags($object_data[$col], $object_data, $obj_type_key, TRUE);
}
}
}
//combine the arrays so we can get a proper mapping
$object_data = array_merge($object_data, $special_curlies);
if (stristr($text, "{BeginList}")) {
$text = netforum_node_process_list_tags($text, $object_data);
}
$text = netforum_node_process_curly_tags($text, $object_data, $obj_type_key);
return $text;
}
/**
* Find and evaluate all {BeginList} and {EndList} tags in the text
*
* The {BeginList} and {EndList} tags must have enough internal tags to
* specify arguments for either an xWeb GetQuery or GetDynamicQuery
* call, the results of which are used in the {BeginDetail} and {EndDetail} tags.
* Nested {BeginList} and {EndList} tags are NOT supported.
*
* @param $text
* A string, the text to be processed
* @param $object_data
* An array or object with fields and replacement values for the fields
* @return
* A string of text with the replacements inserted
* @see netforum_node_process_text()
* @see netforum_node_process_link_tags()
* @see netforum_node_process_curly_tags()
*/
function netforum_node_process_list_tags($text, $object_data = array() ) {
// Start with the sanity checks
if ($text == '' || stristr($text, "{") == false ) {
return $text;
}
if (is_array($object_data) == false) {
$object_data = get_object_vars($object_data);
}
if (is_array($object_data) == false) {
return $text;
}
global $user;
$show_debug = (user_access('administer site configuration') || user_access('create netFORUM node') || user_access('edit netFORUM nodes') || (user_access('edit own netFORUM nodes') && ($user->uid == $node->uid)) );
$lists_pattern = "/\{BeginList\}((?!\{BeginList).+?)\{EndList\}/is";
preg_match_all($lists_pattern, $text, $lists);
$num_lists = count($lists[0]);
$cache_time = variable_get('netforum_cache_default', '1 hour');
// only look in the default cache, if we're refreshing the information we don't want really old (possibly bad) data
for ($index=0; $index < $num_lists; $index++ ) {
$segment = $lists[1][$index]; // This is the text between the {BeginList} and {EndList} tags
$replacement = '';
$szObjectName = '';
$szColumnList = '';
$szWhereClause = '';
$szOrderBy = '';
$szQueryName = '';
$detail_text = '';
// Find all the internal directives in the text, and start with the easiest stuff
preg_match("/\{BeginObjectName\}(.+?)\{EndObjectName\}/is", $segment, $object_names);
preg_match("/\{BeginQueryName\}(.+?)\{EndQueryName\}/is", $segment, $query_name);
preg_match("/\{BeginDetail\}(.+?)\{EndDetail\}/is", $segment, $details);
if (count($object_names) != 0) {
$szObjectName = $object_names[1];
}
else if ($show_debug) {
drupal_set_message(t("Missing {BeginObjectName} and {EndObjectName} tags in list !num.", array('!num' => ($index+1) )), 'error');
}
if (count($query_name) != 0) {
$szQueryName = $query_name[1];
}
if (count($details) != 0) {
$detail_text = $details[1];
}
else if ($show_debug) {
drupal_set_message(t("Missing {BeginDetail} and {EndDetail} tags in list !num.", array('!num' => ($index+1) )), 'error');
}
//If we have enough directives to call GetDynamicQuery do it and stop searching for other directives
if ($szObjectName != '' && $szQueryName != '' && $detail_text != '') {
$results = netforum_xweb_request('GetDynamicQuery', array('szObjectName' => $szObjectName, 'szQueryName' => $szQueryName), $cache_time );
if ($results) {
foreach ($results->Result as $query_result) {
$query_result = get_object_vars($query_result);
$query_result = array_merge($object_data, $query_result);
$replacement .= netforum_node_process_curly_tags($detail_text, $query_result, netforum_object_key($szObjectName) );
}
}
else if ($show_debug) {
drupal_set_message(t("No results for the !obj_name list, make sure that GetDynamicQuery for !query_name works in !xweb_test", array('!obj_name' => $szObjectName, '!query_name' => $szQueryName, '!xweb_test' => l(t('xWeb', 'admin/settings/netforum/xwebtest')) )), 'error');
}
}
else { //Get all the other directives and parse them out
preg_match("/\{BeginColumns\}(.+?)\{EndColumns\}/is", $segment, $columns);
preg_match("/\{BeginWhere\}(.+?)\{EndWhere\}/is", $segment, $where_clauses);
preg_match("/\{BeginOrderBy\}(.+?)\{EndOrderBy\}/is", $segment, $order_by);
if (count($columns) != 0) {
$szColumnList = $columns[1];
}
else if ($show_debug) {
drupal_set_message(t("Missing {BeginColumns} and {EndColumns} tags in list !num.", array('!num' => ($index+1) )), 'error');
}
if (count($where_clauses) != 0) {
$szWhereClause = $where_clauses[1];
}
else if ($show_debug) {
drupal_set_message(t("Missing {BeginWhere} and {EndWhere} tags in list !num.", array('!num' => ($index+1) )), 'error');
}
if (count($order_by) != 0) {
$szOrderBy = $order_by[1];
}
if ($szObjectName != '' && $szWhereClause != '' && $szColumnList != '' && $detail_text != '') {
$arguments = array(
'szObjectName' => $szObjectName,
'szWhereClause' => netforum_node_process_curly_tags($szWhereClause, $object_data),
'szColumnList' => $szColumnList,
'szOrderBy' => $szOrderBy,
);
$results = netforum_xweb_request('GetQuery', $arguments, $cache_time);
if ($results) {
$obj_name = netforum_clean_object_name($szObjectName) ."Object";
foreach ($results->{$obj_name} as $query_result) {
$query_result = get_object_vars($query_result);
$query_result = array_merge($object_data, $query_result);
$replacement .= netforum_node_process_curly_tags($detail_text, $query_result, netforum_object_key($szObjectName) );
}
}
if ($show_debug && $replacement == '') {
drupal_set_message(t("No results for the !obj_name list, make sure that GetQuery works like it should in !xweb_test", array('!obj_name' => $szObjectName, '!xweb_test' => l(t('xWeb'), 'admin/settings/netforum/xwebtest') )), 'error');
}
}
else if ($show_debug && $szWhereClause == '' && $szColumnList == '') {
drupal_set_message(t("No output for list number !num, perhaps you meant to add {BeginQueryName} and {EndQueryName}?", array('!num' => ($index+1) )), 'error');
}
}
$text = str_ireplace($lists[0][$index], $replacement, $text);
}
return $text;
}
/**
* Replace all {BeginLink} and {EndLink} tags with a link to a node
*
* For use within lists any text between the {BeginLink} and {EndLink} tags will
* be linked to a node with the current object key if it exists, or link to a new
* node with that object key if a template for that object exists.
*
* @param $text
* A string with the text to be searched and replaced
* @param $object_data
* An array or object with the fields and values to be inserted
* @param $obj_type_key
* A string with the guid of the object type to link to
* @return
* A string with links inserted
* @see netforum_node_process_text()
* @see netforum_node_process_list_tags()
* @see netforum_node_process_curly_tags()
*/
function netforum_node_process_link_tags($text, $object_data, $obj_type_key) {
//start with some basic sanity checks
$obj_type_key = (string)$obj_type_key;
if ($text == '' || stristr($text, "{") == false ) {
return $text;
}
if (is_array($object_data) == false) {
$object_data = get_object_vars($object_data);
}
if (is_array($object_data) == false) {
return $text;
}
// some things aren't likely to change in during the request, so hang onto them and avoid database calls
static $prefix_cache = array();
static $template_cache = array();
if (array_key_exists($obj_type_key, $prefix_cache)) {
$obj_prefix = $prefix_cache[$obj_type_key];
}
else{
$obj_prefix = db_result(db_query("SELECT obj_prefix FROM {netforum_object_cache} WHERE obj_key = '%s'", $obj_type_key));
$prefix_cache[$obj_type_key] = $obj_prefix;
}
$links_pattern = "/\{BeginLink\}(.+?)\{EndLink\}/is";
preg_match_all($links_pattern, $text, $links);
$num_links = count($links[0]);
for ($index=0; $index < $num_links; $index++ ) {
$segment = $links[1][$index]; //This is the text in between the {BeginLink} and {EndLink} tags
$replacement = '';
//If there are any conditionals or curlies in the link text, figure it out
$link_text = netforum_node_process_curly_tags($segment, $object_data);
if ($obj_prefix) {
// Find the objects primary key, first checking for prefix_key, and if that fails prefix_cst_key
$nfn_key = $object_data[$obj_prefix ."_key"] ? $object_data[$obj_prefix ."_key"] : $object_data[$obj_prefix ."_cst_key"] ;
$nid = db_result(db_query("SELECT nid FROM {netforum_node} WHERE nfn_key = '%s' LIMIT 1", $nfn_key));
if ($nid) { //If a node with that object key and type exists, link to it. If there's more than one node with the same key and object, well, good luck.
$replacement .= l($link_text, 'node/'. $nid);
}
else { //If no node exists, find out if we have a template
if (array_key_exists($obj_type_key, $template_cache)) {
$template_count = $template_cache[$obj_type_key];
}
else {
$template_count = db_result(db_query("SELECT count(nft_obj_key) FROM {netforum_node_types} WHERE nft_obj_key = '%s'", $obj_type_key));
$template_cache[$obj_type_key] = $template_count;
}
if ($template_count > 0) { //If there is a template for it, link it. If a request for a node comes in with two valid guids it will create the node with netforum_node_create_from_template
$replacement .= l($link_text, 'node/'. $obj_type_key ."/". $nfn_key );
}
else {
$replacement .= $link_text; //if there's no template there's no link.
}
}
}
else{ //If we have no object or no object prefix, then we have nothing to link to
$replacement .= $link_text;
}
$text = str_ireplace($links[0][$index], $replacement, $text);
}
return $text;
}
/**
* Replace object columns inside curly brackets and conditionals with the object values
*
* Treat text like the correspondence template or HTML content type on eWeb forms by replacing
* curly brackets with appropriate data depending on the object passed. If no object is presented
* then any object columns will be replaced with empty values.
*
* @param $text
* A string with the plaint text to be replaced
* @param $object_data
* A hashed array representing an object with keys set to column names and values set to the column data.
* @param $obj_type_key
* A string, if set the text will call netforum_node_process_list_tags to process {BeginLink} and {EndLink} tags
* @param $ignore_html_tag
* Boolean, if set to true any tag with html in the name will be replaced with an empty string.
* @return
* A string with the parsed text
* @see netforum_node_process_text()
* @see netforum_node_process_list_tags()
* @see netforum_node_process_link_tags()
*/
function netforum_node_process_curly_tags($text, $object_data = array(), $obj_type_key = '', $ignore_html_tag = false ) {
if ($text == '' || stristr($text, "{") == false ) {
return $text;
}
if (is_array($object_data) == false) {
$object_data = get_object_vars($object_data);
}
if (is_array($object_data) == false) {
return $text;
}
$simple_curly_pattern = '/\{[^\?]([^\{ \}]*) \}/x';
$conditional_curly_pattern = '/\{\?([^\{\}]*)\}/x';
if (stristr($text, "{BeginLink}") && netforum_is_valid_guid($obj_type_key) ) {
$text = netforum_node_process_link_tags($text, $object_data, $obj_type_key);
}
//Do a case insensitive search and replace with two arrays, one is the object keys with { } added on each side and the other is the object values.
$text = str_ireplace( array_map(create_function('$v', 'return "{". $v ."}";'), array_keys($object_data)), array_values($object_data), $text);
//Replace any simple curly brackets with an empty string
$text = preg_replace($simple_curly_pattern, '', $text);
// now we get to dealing with conditional statements. Since they can be nested, we parse them out in a
// while loop, but step one is priming the loop. We deal with them inside out, evaluating the simplest statements
// before evaluating the complex nested ones. So in a conditional that looks like this:
// {?cst_type=='Individual'?{?cst_name=='fred'?Hi fred?where's fred}?you're a company, even if you are named fred}
// we evaluate {?cst_name=='fred'?Hi fred?where's fred} on the first loop, leaving us
// {?cst_type=='Individual'?Hi fred?you're a company, even if you are named fred}
// to evaluate on the second iteration
preg_match_all($conditional_curly_pattern, $text, $conditional_matches);
while (count($conditional_matches[0]) > 0) {
//as long as we keep getting hits on the logic (eg, conditional matches), keep replacing them.
$searches = array();
$replacements = array();
foreach ($conditional_matches[0] as $conditional) {
$searches[] = $conditional; // this is the complete conditional statement from { to }, which we will replace once we figure out what to replace it with.
$parts = explode('?', $conditional); //the eval/true/false parts
//replace any known values in the evaluation part with the data, eg converting from cst_type to 'Customer' in the example above. Include double quotes around it and add slashes too.
$condition = str_ireplace(array_keys($object_data), array_values($object_data), $parts[1]);
$condition_result = _netforum_node_parse_conditional($condition);
if ($condition_result == true && isset($parts[2]) ) {
$replacements[] = $parts[2]; //these are the parts of the complete statement from { to }, so we're replacing the true part or the false part as appropriate
}
elseif (isset($parts[3])) {
$replacements[] = trim($parts[3], "{}");
}
else {
$replacements[] = '';
}
}
$text = str_replace($searches, $replacements, $text);
//search out any more matches and repeat if needed
preg_match_all($conditional_curly_pattern, $text, $conditional_matches);
}
return $text;
}
/**
* Evaluate the truth of a conditional statement as found in the netFORUM template syntax
*
* Designed to work with the netFORUM HTML template syntax this evaluates a statement without
* using the insecure eval() function. It does strip beginning and ending spaces and quotes from the string, so
* a single space is logically the same as empty. It is a little more tolerant in that you can have spaces between
* the comparison operator
*
* @param
* String with the test to be evaluated
* @return
* Boolean. True if the string can be evaluated and evaluates to true, false in all other cases
* @see netforum_node_process_text()
*/
function _netforum_node_parse_conditional($cond_str) {
$conditionals = array('==', '!=', '<=', '>=', '<>', '>', '<', '===', '!==');
foreach ($conditionals as $c) {
$pos = strpos($cond_str, $c);
if ($pos !== false) {
list($lcomp, $rcomp) = explode($c, $cond_str);
$cond = $c;
$lcomp = trim($lcomp, " \"'");
$rcomp = trim($rcomp, " \"'");
break; //break the loop because we are only looking for one conditional to match, no compounds
}
}
if (isset($cond) == false) {
return false; //If we were called without a conditional for some reason return false
}
switch ($cond) { //you may see a pattern here...
case '==':
return ($lcomp == $rcomp);
case '!=':
return ($lcomp != $rcomp);
case '<=':
return ($lcomp <= $rcomp);
case '>=':
return ($lcomp >= $rcomp);
case '<>':
return ($lcomp <> $rcomp);
case '>':
return ($lcomp > $rcomp);
case '<':
return ($lcomp < $rcomp);
case '===':
return ($lcomp === $rcomp);
case '!==':
return ($lcomp !== $rcomp);
default:
return false;
}
}
/*
*******************************************************************************
*******************************************************************************
Special Field Functions
*******************************************************************************
*******************************************************************************
*/
/**
* Get a list of special fields for use in netforum_node_process_text()
*
* The special fields are editable by the user at admin/settings/netforum/special-fields,
* but a few are set up by default. These will be included on every call to process text
*
* @param $descriptions
* Boolean defaults to false. If it is set true it will return the field descriptions instead of the values
* @return
* A hashed array with the field names and either values or descriptions depending on the $descriptions variable
*/
function netforum_node_special_fields($descriptions = false) {
static $description_fields = array();
static $value_fields = array();
$fields = array();
if ($descriptions) {
if (count($description_fields) > 0) {
return $description_fields;
}
else{
$res = db_query("SELECT field_code, field_description FROM {netforum_node_special_fields}");
while ($fld = db_fetch_array($res)) {
$fields[$fld['field_code']] = "SpecialField::";
$fields[$fld['field_code']] .= trim($fld['field_description']) == '' ? $fld['field_code'] : $fld['field_description'];
}
$description_fields = $fields;
}
}
else {
if (count($value_fields) > 0) {
return $value_fields;
}
else {
$res = db_query("SELECT field_code, field_value FROM {netforum_node_special_fields}");
while ($fld = db_fetch_array($res)) {
$fields[$fld['field_code']] = netforum_node_special_field_value($fld['field_code'], $fld['field_value']);
}
$value_fields = $fields;
}
}
return $fields;
}
/**
* Get the value of the given special field
*
* Special fields can optionally include PHP code, this function looks for that and if found
* executes the PHP.
*
* @param $field_code
* A string with the field code
* @param $field_value
* A string, if not set the value from the database is used
* @return
* A string with the field value
*/
function netforum_node_special_field_value($field_code, $field_value = null) {
if (is_null($field_value)) {
$fld = db_fetch_array(db_query("SELECT field_value FROM {netforum_node_special_fields} WHERE field_code = '%s'", $field_code));
$field_value = $fld['field_value'];
}
if ($field_code && $field_value) {
$field_value = trim($field_value);
if (strtolower(substr($field_value, 0, 5)) == "field_code] = $fld;
}
$header = array(t('Code'), t('Description'), t('Value'), array('data' => t('Operations'), 'colspan' => '2'));
$rows = array();
foreach ($fields as $fld ) {
$code = check_plain($fld->field_code);
$description = check_plain($fld->field_description);
$value = netforum_node_special_field_value($code, $fld->field_value);
// Populate the operations field.
$operations = array();
// Set the edit column.
$operations[] = array('data' => l(t('edit'), 'admin/settings/netforum/special-fields/'. $code));
// Set the delete column.
$operations[] = array('data' => l(t('delete'), 'admin/settings/netforum/special-fields/'. $code .'/delete'));
$row = array(l($code, 'admin/settings/netforum/special-fields/'. $code),
check_plain($description),
check_plain($value));
foreach ($operations as $operation) {
$row[] = $operation;
}
$rows[] = $row;
}
if (empty($rows)) {
$rows[] = array(array('data' => t('No special fields available.'), 'colspan' => '5', 'class' => 'message'));
}
return theme('table', $header, $rows);
}
/**
* The form for adding or editing a special field
*/
function netforum_node_special_field_form($field_code = NULL) {
$field_description = '';
$field_value = '';
if ($field_code != null) {
$field_details = db_fetch_array(db_query("SELECT field_description, field_value from {netforum_node_special_fields} WHERE field_code = '%s'", $field_code));
$field_description = $field_details['field_description'];
$field_value = $field_details['field_value'];
}
$form['field_code'] = array(
'#type' => 'textfield',
'#title' => t('Special field name'),
'#size' => 50,
'#required' => TRUE,
'#weight' => -7,
'#default_value' => $field_code,
'#description' => t('The one-word code that will be used in netFORUM nodes'),
);
$form['field_description'] = array(
'#type' => 'textfield',
'#title' => t('Special field description'),
'#size' => 60,
'#maxlength' => 50,
'#weight' => -3,
'#default_value' => $field_description,
'#description' => t('A short, informative sentence about the special field'),
);
$form['field_value'] = array(
'#type' => 'textfield',
'#title' => t('Special field value') ,
'#required' => TRUE,
'#default_value' => $field_value,
'#weight' => -5,
'#description' => t('Any PHP code entered will be evaluated, just be sure to start with <?php and print some output.'),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save special field'),
);
if (!empty($field_code)) {
$form['delete'] = array(
'#type' => 'submit',
'#value' => t('Delete special field'),
);
$form['old_field_code'] = array(
'#type' => 'hidden',
'#value' => $field_code,
);
}
return $form;
}
/**
* hook_validate for the special field form
*/
function netforum_node_special_field_form_validate($form, &$form_state) {
/* TODO The 'op' element in the form values is deprecated.
Each button can have #validate and #submit functions associated with it.
Thus, there should be one button that submits the form and which invokes
the normal form_id_validate and form_id_submit handlers. Any additional
buttons which need to invoke different validate or submit functionality
should have button-specific functions. */
$op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
if ( $form_state['values']['field_code'] && stristr(trim($form_state['values']['field_code']), " ") != false ) {
form_set_error('field_code', t('Special field codes cannot contain spaces'));
}
if ( $form_state['values']['field_code'] && (stristr($form_state['values']['field_code'], "}") != false || stristr($form_state['values']['field_code'], "{") != false)) {
form_set_error('field_code', t('Special field codes cannot contain curly brackets like { or }'));
}
if ( empty($form_state['values']['old_field_code'])) {
$is_existing = db_result(db_query("SELECT * FROM {netforum_node_special_fields} WHERE field_code = '%s'", $form_state['values']['field_code']));
if ($is_existing) {
form_set_error('field_code', t('The %code special field code already exists.', array("%code" => check_plain($form_state['values']['field_code']))));
}
}
}
/**
* Implementation of hook_form_submit().
*/
function netforum_node_special_field_form_submit($form, &$form_state) {
/* TODO The 'op' element in the form values is deprecated.
Each button can have #validate and #submit functions associated with it.
Thus, there should be one button that submits the form and which invokes
the normal form_id_validate and form_id_submit handlers. Any additional
buttons which need to invoke different validate or submit functionality
should have button-specific functions. */
$op = isset($form_state['values']['op']) ? $form_state['values']['op'] : '';
$fld = new stdClass();
$fld->field_code = trim($form_state['values']['field_code']);
$fld->field_description = trim($form_state['values']['field_description']);
$fld->field_value = trim($form_state['values']['field_value']);
if ($op == t('Delete special field')) {
return 'admin/settings/netforum/special-fields/'. $fld->field_code .'/delete';
}
$status = netforum_node_special_field_save($fld);
if ($status == SAVED_UPDATED) {
drupal_set_message(t('The %code special field has been updated.', array('%code' => $fld->field_code)));
}
elseif ($status == SAVED_NEW) {
drupal_set_message(t('The %code special field has been added.', array('%code' => $fld->field_code)));
watchdog('netforum', 'Added %code special field.', array('%code' => $fld->field_code), WATCHDOG_NOTICE, l(t('view'), 'admin/settings/netforum/special-fields'));
}
$form_state['redirect'] = 'admin/settings/netforum/special-fields';
}
/**
* Save or update the special field
*
* @param $info
* An object representing the special field
* @return
* SAVED_NEW or SAVED_UPDATED
*/
function netforum_node_special_field_save($info) {
$is_existing = FALSE;
$existing_field = !empty($info->field_code) ? $info->field_code : NULL;
if ($existing_field) {
$is_existing = db_result(db_query("SELECT * FROM {netforum_node_special_fields} WHERE field_code = '%s'", $existing_field));
}
if ($is_existing) {
db_query("UPDATE {netforum_node_special_fields} SET field_value = '%s', field_description = '%s' WHERE field_code = '%s'", $info->field_value, $info->field_description, $existing_field );
return SAVED_UPDATED;
}
else {
$res = db_query("INSERT INTO {netforum_node_special_fields} (field_code, field_value, field_description) VALUES('%s', '%s', '%s') ", $existing_field, $info->field_value, $info->field_description);
if (db_affected_rows($res) > 0) {
return SAVED_NEW;
}
}
}
/**
* hook_delete_confirm() for special fields
*/
function netforum_node_special_field_delete_confirm( $field_code) {
$form['field_code'] = array('#type' => 'value', '#value' => $field_code);
$message = t('Are you sure you want to delete the %code special field code?', array('%code' => $field_code));
$caption = '
Nodes can also use syntax similar to the List content type in eWeb. Here is an example to pull all email addresses for an Individual:
{BeginList}
{BeginObjectName}IndividualEmail{EndObjectName}
{BeginWhere}eml_cst_key = "{ind_cst_key}" {EndWhere}
{BeginColumns}*{EndColumns}
{BeginOrderBy}eml_type, eml_address {EndOrderBy}
<ul>
{BeginDetail}<li>{eml_address}, {BeginLink}{eml_type}{EndLink} <li>{EndDetail}
</ul>
{EndList}
The object name, columns, where statement, and order by are passed to xWeb, so if no results appear try your query in the !xweb_test page, being careful which columns you select.
Any text between the {BeginLink} and {EndLink} tags is checked for curly bracket codes, and the resulting text is linked to a node on the website if one of the following is true:
{BeginList}
{BeginObjectName}Individual{EndObjectName}
{BeginQueryName}Newest members{BeginQueryName}
{BeginDetail}<li>{ind_first_name} joined on {ind_add_date}</li>{EndDetail}
{EndList}
The list will only return valid results if the iWeb query has no ask at runtime parameters.