background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In this quick snippet, I'll show some code that uses the Tika Java library to index the content in WebFM file attachments and add the data as Apache Solr documents. This code is designed to work with the Apache Solr Search Integration Drupal module, and piggyback off of the Apache Solr Attachments module. Out of the box, the Apache Solr Attachments module can index CCK file fields and node file attachments, but the WebFM module uses its own custom file tables and therefore the files are not indexed. This code assumes you have the Tika library already integrated with your Solr installation. Please review the Tika Getting Started documentation for more information.

<?php
/**
* Implements hook_update_index()
*/
function index_webfm_update_index() {

 
$cron_limit = 10;
 
$rows = apachesolr_get_nodes_to_index('index_webfm', $cron_limit);
 
$success = apachesolr_index_nodes($rows, 'index_webfm', '_index_webfm_add_documents');

}

/**
* Implements custom function to add webfm file attachments content as Solr documents
*/
function _index_webfm_add_documents(&$documents, $nid, $namespace = 'index_webfm') {
  if (
$namespace != 'index_webfm') {
    return;
  }

 
// load node
 
$node = node_load($nid);
  if (!
is_object($node)) {
    return;
  }

 
// get webfm fids
 
$sql = "
    select a.fid, f.fpath as `filepath`, f.fmime as `filemime`, f.fcreatedate as `timestamp`
    from {webfm_attach} a
    join {webfm_file} f on f.fid = a.fid
    where a.nid = %d
    order by a.weight
  "
;
 
$resource = db_query($sql, $node->nid);
 
$webfm_data = array();
  while (
$row = db_fetch_object($resource)) {
   
$webfm_data[] = $row;
  }
 
 
// ensure webfm files exists for node
 
if (!count($webfm_data)) {
    return;
  }
 
 
// loop through web fm data, collect data to add to document
 
$tika_data = NULL;
  foreach (
$webfm_data as $file) {
   
   
// direct tika
   
if (variable_get('apachesolr_attachment_extract_using', 'tika') == 'tika') {
     
      if (
function_exists('apachesolr_attachments_extract_using_tika')) {
       
$tika_data = apachesolr_attachments_extract_using_tika($file->filepath);
      }
     
    }
   
// tika via solr
   
else {
     
      if (
function_exists('apachesolr_attachments_extract_using_solr')) {
        list(
$tika_data, $metadata) = apachesolr_attachments_extract_using_solr($file->filepath);
      }
     
    }
    if (!
$tika_data || !is_string($tika_data)) {
      continue;
    }
   
   
// create new Solr document
   
$document = new Apache_Solr_Document();
   
$document->id = apachesolr_document_id($file->fid .'-'. $node->nid, 'file');
   
$document->url = file_create_url($file->filepath);
   
$document->path = $file->filepath;
   
$document->hash = apachesolr_site_hash();
   
$document->entity = 'file';
   
$document->site = url(NULL, array('absolute' => TRUE));
   
$document->nid = $node->nid;
   
$document->title = basename($file->filepath);
   
$document->created = apachesolr_date_iso($file->timestamp);
   
$document->changed = $document->created;
   
$document->status = $node->status;
   
$document->sticky = $node->sticky;
   
$document->promote = $node->promote;
   
$document->uid = $node->uid;
   
$document->name = $node->name;
   
$document->body = apachesolr_clean_text(basename($file->filepath) . ' ' . $tika_data);
   
$document->ss_filemime = $file->filemime;
   
$document->ss_file_node_title = apachesolr_clean_text($node->title);
   
$document->ss_file_node_url = url('node/' . $node->nid, array('absolute' => TRUE));
  
   
// add new webfm document to documents
   
$documents[] = $document;
       
  }
}
?>

After implementing this code, when nodes were set to be indexed by Solr, their webfm file attachments were separately processed, the content was extracted from the file attachments, and added as new Solr documents.

In this article, I'll address an issue that has surfaced on every recent project I've been working on: how to update nodes when a CCK field's allowed values list changes. For my projects, I implemented a variety of batch processes and drush scripts to solve this problem. But for this article, I decided to implement a form submit handler solution to automatically update nodes when the CCK field settings page is submitted. Please be careful when implementing code like this, it has potential to update a lot of nodes, and hit memory limitation issues depending on how many nodes need to be updated. As I mentioned before, I implemented a combination of batch APIs and drush scripts to process production environments; I implemented this code in a development environment. Anyway..

I created a new content type "band", and added a CCK text/select field (genre) with an allowed values list:

Allowed value list before

I used Devel's auto-generate content functionality to create 20 nodes (/admin/generate/content), and then added a simple view to show the node's title and genre:

view of nodes before

Now that I had some working test data, I added some code to a custom module to modify the CCK field property settings page, and add a submit handler update the nodes:

<?php
// define module constants
define('CCK_FIELD_PROCESS', 'field_genre');
define('CCK_TYPE_PROCESS', 'band');

/**
* Implements hook_form_alter()
*/
function helper_form_alter(&$form, &$form_state, $form_id) {
 
 
// check for CCK form, CCK type, and CCK field (see above constants)
 
if ($form_id == 'content_field_edit_form' && $form['type_name']['#value'] == CCK_TYPE_PROCESS && $form['field_name']['#value'] == CCK_FIELD_PROCESS) {
   
   
// get original allowed values
   
$original_allowed_values_string = $form['field']['allowed_values_fieldset']['allowed_values']['#default_value'];
   
   
// explode values, get array of values   
   
$exploded = explode("\n", $original_allowed_values_string);
   
$original_allowed_values_array = array();
    foreach (
$exploded as $data) {

     
// explode data on "|"
     
list($key, $value) = $exploded2 = explode('|', $data);     
     
$original_allowed_values_array[$key] = $value;
     
    }
   
   
// ensure data exists
   
if (empty($original_allowed_values_array)) {
      return;
    }

   
// store original values in form state storage
   
$form_state['storage']['original_allowed_values'] = $original_allowed_values_array;
   
   
// store other information in form state
   
$form_state['storage']['cck_type'] = CCK_TYPE_PROCESS;
   
$form_state['storage']['cck_field'] = CCK_FIELD_PROCESS;

   
// add submit handler
   
$form['#submit'][] = '_helper_form_alter_content_field_edit_form_submit';
       
  }

}

/**
* Implements custom form submit handler
*/
function _helper_form_alter_content_field_edit_form_submit($form, &$form_state) {

 
// get new allowed values list
 
$new_allowed_values_string = $form_state['values']['allowed_values'];
 
 
// ensure data exists
 
if (empty($new_allowed_values_string)) {
    return;
  }

 
// explode values, get array of values   
 
$exploded = explode("\n", $new_allowed_values_string);
 
$new_allowed_values_array = array();
  foreach (
$exploded as $data) {

   
// explode data on "|"
   
list($key, $value) = $exploded2 = explode('|', $data);     
   
$new_allowed_values_array[$key] = $value;
   
  } 

 
// ensure data exists
 
if (empty($new_allowed_values_array)) {
    return;
  }
 
 
// NOTE: the next few lines gets the key values from the allowed values
  // and checks to see which are different.
  // This code assumes any change to the keys (including changing the order) will update the nodes.
  // Update to your heart's content
 
$new_keys = array_keys($new_allowed_values_array);
 
$old_keys = array_keys($form_state['storage']['original_allowed_values']);
 
 
$key_diffs = array();
  foreach (
$old_keys as $key => $value) {
   
    if (
$new_keys[$key] != $value) {
     
$key_diffs[$value] = $new_keys[$key];
    }
   
  }
 
 
// ensure data has changed
 
if (empty($key_diffs)) {
    unset(
$form_state['storage']);
    return;
  }
 
 
// get data from form storage
 
$cck_field = $form_state['storage']['cck_field'];
 
$cck_type = $form_state['storage']['cck_type'];
 
 
// loop through key diffs
 
foreach ($key_diffs as $old => $new) {
 
   
// get a list of node ids that need to be updated
    // NOTE: this sql assumes the field is NOT shared
   
$sql = "
      select n.nid
      from {node} n
      join {content_type_%s} b on b.nid = n.nid and b.vid = n.vid
      where n.type = '%s' and b.%s_value = '%s'
    "
;
   
$resource = db_query($sql, $cck_type, $cck_type, $cck_field, $old);
   
$node_ids = array();
    while (
$row = db_fetch_object($resource)) {
     
$node_ids[] = $row->nid;
    }
   
   
// ensure results exist
   
if (empty($node_ids)) {
      continue;
    }
   
   
// loop through node ids
    // NOTE: got a lot of nodes? you might need a batch API instead
   
foreach ($node_ids as $nid) {
     
     
// load node
     
$node = node_load($nid, NULL, TRUE);
     
     
// ensure node loaded
     
if (!is_object($node)) {
        continue;
      }
     
     
// update node properties
     
$node->{$cck_field}[0]['value'] = $new;
     
$node->revision = TRUE;
     
node_save($node);
     
     
// remove cck cache for node
     
db_query("DELETE FROM {cache_content} WHERE cid LIKE 'content:%d%'", $node->nid);
     
    }
 
  }
 
 
// remove form storage
 
unset($form_state['storage']);

}
?>

To test this code, I went back to the CCK field settings page (example: /admin/content/node-type/band/fields/field_genre), updated the allowed value list, and saved.

updated allowed value list

The form submit handler is triggered and the nodes are updated. Refreshing my view shows the updated genre fields on my test nodes:

Updated node list

Eric.London's picture

I've encountered this issue a few times recently when working with Apache Solr (in Drupal 6): clients want to to show all search results and allow the users to browse facets, instead of requiring a search phrase or filter to begin the search. The current version of the Apache Solr module appears to require at least one of these items.

A quick look at apachesolr_search.module, in the function: apachesolr_search_view() shows this is not going to happen:

<?php
$keys
= trim(search_get_keys());
// ...snip...
$filters = trim($_GET['filters']);
// ...snip...
if ($keys || $filters) {
 
// do search
}
?>

So, what if there was a way to modify the $_GET values prior to the apache solr search page callback? hook_boot maybe? NOTE: hook_boot functions are only processed if the bootstrap column in the system table for your module is set to 1! Example SQL to update..

update {system}
set bootstrap = 1
where type = 'module' and name = 'MYMODULE'

I then created this hook_boot function in my module. It is run prior to the the apache solr search page callback, and allows me to tamper with $_GET variables. In this case, I just add a fake filter to allow the search to pass the above conditional statements.

<?php
function MYMODULE_boot() {
 
 
// check for search page
 
$search_path = 'search/apachesolr_search';
  if (
substr($_GET['q'], 0, strlen($search_path))==$search_path) {
   
   
// get search phrase
   
$search_phrase = substr($_GET['q'], strlen($search_path)+1);
   
   
// get filters
   
$filters = $_GET['filters'];
   
   
// check if filters AND search phrase do not exist
   
if (empty($filters) && empty($search_phrase)) {
         
     
// set fake filter
     
$_GET['filters'] = 1;
     
    }
  }
 
}
?>

Now, going to the solr search page defaults to show all results and facets:

Solr All Results

It appears to work, but anyone know of a better way to accomplish this?! :)

In this article, I'll share some code that I wrote that uses Appcelerator Titanium to create a simple iPhone application that communicates with a Drupal site via SOAP. From the iPhone side of things I used the Suds Javascript SOAP client, and from Drupal, I created a nusoap soap server instance.

Let's start with the SOAP server instance. I created a module called "nusoap", and copied the lib folder from nusoap into it. I then created a file in the module's directory called "soap-server.php" and added the following code. For the sake of this article, I did not include the rest of the Drupal module's code.

<?php
// $Id$

// define namespace
define('NUSOAP_NAME_SPACE', 'erl.dev');

// define path to nusoap library file
$nu_soap_path = 'lib/nusoap.php';

// ensure nu_soap library exsists
if (!file_exists($nu_soap_path)) {
  die(
'An error has occurred initializing the soap server.');
}

// include nu_soap library
require_once ($nu_soap_path);

// create new soap server instance
$soap_server = new nusoap_server();

// configure wsdl
$soap_server->configureWSDL(NUSOAP_NAME_SPACE, 'urn:'. NUSOAP_NAME_SPACE);

$soap_server->register(
 
// method name
 
'login_fetch_data',
 
// input args
 
array(
   
'name' => 'xsd:string',
   
'pass' => 'xsd:string',
  ),
 
// output args
 
array(
   
'return' => 'xsd:string'
 
),
 
// namespace
 
'uri:'. NUSOAP_NAME_SPACE,
 
// SOAPAction
 
'uri:'. NUSOAP_NAME_SPACE .'#login_fetch_data',
 
// style
 
'rpc',
 
// use
 
'encoded'
);

// change directory to docroot
chdir($_SERVER['DOCUMENT_ROOT']);

// drupal bootstrap
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

// process raw post data
$HTTP_RAW_POST_DATA = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : '';
$soap_server->service($HTTP_RAW_POST_DATA);

/**
* Define soap methods
*/

function login_fetch_data($name, $pass) {
 
 
$params = array(
   
'name' => $name,
   
'pass' => $pass,
  );
 
 
$account = user_authenticate($params);
 
 
// check for error
 
if (!is_object($account)) {
   
// handle authentication error here
    // return something
 
}
   
 
// return user uid, or some other piece of data
 
return $account->uid;
 
}
?>

The above code registers one soap server method "login_fetch_data" which accepts the arguments (name and pass), loads a full Drupal bootstrap, authenticates the user, and returns back the user's uid. By browsing to the soap server file directly, I can see nusoap's WSDL definition: (example: http://drupal.vm/sites/all/modules/custom/nusoap/soap-server.php)

SOAP Server Methods

To test if the soap server method was working, I used the follow PHP code snippet (which I pulled out of a test page callback):

<?php
// define path to nu_soap library, relative to module directory
define('NUSOAP_PATH', drupal_get_path('module', 'nusoap') .'/lib/nusoap.php');

// define namespace
define('NUSOAP_NAME_SPACE', 'erl.dev');

// include nu_soap library
require_once(NUSOAP_PATH);

// define wsdl path
$wsdl_path = 'http://' . $_SERVER['HTTP_HOST'] . base_path() . drupal_get_path('module', 'nusoap') . '/soap-server.php?wsdl';

// create new soap client instance
$soap_client = new nusoap_client($wsdl_path, true);

// check for error
$error = $soap_client->getError();
if (
$error) {
 
// handle error
}
 
$args = array(
 
'name' => 'Eric.London',
 
'pass' => 'super secret password',
);

// call soap server method
$result = $soap_client->call('login_fetch_data', $args);

$output = "";
$output .= "<pre>";
$output .= "SENT: ";
$output .= print_r($args, true);
$output .= "RECEIVED: ";
$output .= print_r($result, true);
$output .= "</pre>";
   
return
$output;
?>

I then created a new Titanium iPhone project. In the Resources directory, I created a directory called "soap-test-includes" to contain the suds.js library, and the soap.js file which uses this library. The suds.js file is available in the Kitchen Sink Titanium project, if you choose to import it.

Titanium folder structure

I added the following code to the app.js file:

// this sets the background color of the master UIView (when there are no windows/tab groups on it)
Titanium.UI.setBackgroundColor('#000');

// create tab group
var tabGroup = Titanium.UI.createTabGroup();

//
// create base UI tab and root window
//
var win1 = Titanium.UI.createWindow({ 
    title:'Soap Test',
    backgroundColor:'#fff',
    url: 'soap-test-includes/soap.js'
});
var tab1 = Titanium.UI.createTab({ 
    icon:'KS_nav_views.png',
    title:'Soap Test',
    window:win1
});

//
//  add tabs
//
tabGroup.addTab(tab1); 

// open tab group
tabGroup.open();

And, I added the following code to the soap.js include file:

// include suds soap library
Titanium.include('suds.js');

var window = Ti.UI.currentWindow;
var label = Ti.UI.createLabel({
  top: 10,
  left: 10,
  width: 'auto',
  height: 'auto',
  text: 'Contacting Drupal soap server...\n'
});

window.add(label);

// define soap params
var url = "http://drupal.vm/sites/all/modules/custom/nusoap/soap-server.php";
var callparams = {
  name: 'Eric.London',
  pass: 'super secret password'
};

// create new suds soap client
var suds = new SudsClient({
  endpoint: url,
  targetNamespace: 'erl.dev'
});

try {
 
  // make soap server call
  suds.invoke('login_fetch_data', callparams, function(xmlDoc) {
 
    // fetch results from method
    results = xmlDoc.documentElement.getElementsByTagName('login_fetch_dataResponse');
      
    // check if results exits
    if (results && results.length>0) {
 
      // output results
      label.text += "User uid: " + results.item(0).text;     

    }
    else {
      // handle error    
    }

  });
} catch(e) {
  Ti.API.error('Error: ' + e);
}

When I build and run the project, the iOS Simulator opens and shows the results of the soap connection!

iOS soap connection

As you can see, Titanium allows you to write an iPhone application without touching the native Objective-C code. In this example, all the code was written in javascript, with a PHP backend. Much of the javascript code I used is based on code found in the Kitchen Sink project; I suggest you check it out :)

Eric.London's picture

In this article, I'll share some code that I use to gray out the screen during AJAX calls to prevent users from clicking on anything, and displays a visual cue to the user that their click request is still being processed.

In the following code, I created a hook_menu() implementation and 2 page callbacks; one to show a clickable link and the other to process the AJAX request.

<?php
/**
* Implements hook_menu()
*/
function helper_menu() {

 
$items = array();

 
// define the page callback to show the clickable link 
 
$items['shadow-test'] = array(
   
'title' => t('Shadow Test'),
   
'page callback' => '_helper_page_callback_shadow_test',
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
 
// define the page callback to process the AJAX request
 
$items['shadow-test-ajax'] = array(
   
'title' => t('Shadow Test Ajax'),
   
'page callback' => '_helper_page_callback_shadow_test_ajax',
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
  return
$items;

}

/**
* Implements the page callback to show the clickable link
*/
function _helper_page_callback_shadow_test() {

 
// include module css
 
drupal_add_css(drupal_get_path('module', 'helper') .'/helper.css');

 
// include module javascript
 
drupal_add_js(drupal_get_path('module', 'helper') .'/helper.js');

 
// generate shadow html
 
$shadow_html = "<div id='shadow' style='background: url(\"" . base_path() . drupal_get_path('module', 'helper') .'/shadow.png' . "\")'></div>";

 
// define javascript variables to be passed to the DOM
 
$js_vars = array(
   
'helper' => array(
     
'ajax_path' => base_path() . 'shadow-test-ajax',
     
'shadow_html' => $shadow_html,
    ),
  );

 
// pass variables to javascript
 
drupal_add_js($js_vars, 'setting');

 
// create a variable for page output
 
$output = "";
 
 
// create a clickable link
  // NOT: this link will be overwritten via jQuery
 
$output .= l(
   
t('Click Me'),
   
$_REQUEST['q'],
    array(
     
'attributes' => array(
       
'class' => 'ajax_clickable',
      ),
    )
  );
 
 
// return page output
 
return $output;

}

/**
* Implements page callback to process the AJAX request
*/
function _helper_page_callback_shadow_test_ajax() {

 
// wait 2 seconds
  // NOTE: this line is just used to demo the "shadow" effect
 
sleep(3);
 
 
// return a JSON value
 
$ret = new StdClass();
 
$ret->status = true;
  print
drupal_json($ret);

  die;

}
?>

After flushing my menu cache, and browsing to the first page callback, I see the following:

Shadow Off

I added some CSS to my module's CSS include file (helper.css), to help position the "shadow" full screen:

#shadow {
  z-index: 1000;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: none;
}

I created a 50% black transparent PNG image in PhotoShop:

50% black image

I added some jQuery to my module's javascript include file (helper.js), to add the click event to the link, and two functions to hide and show the shadow.

Drupal.behaviors.helper = function(context) {

  // find the link with the matching class, and add a click event
  $('.ajax_clickable').click(function(){
   
    // show shadow
    $(this).show_shadow();

    // make ajax call   
    $.getJSON(
      Drupal.settings.helper.ajax_path,
      function(data) {
       
        // when the ajax call is done, hide the shadow
        $(this).hide_shadow();
       
      }
    );

    // prevent the a tag from actually going anywhere
    return false;
 
  });
 
}

;(function($) {

  // defines the function to show the shadow
  $.fn.show_shadow = function() {
 
    // ensure the shadow does not already exist
    if ($('body #shadow').length == 0) {
     
      // add shadow html, and fade it in
      $('body').append(Drupal.settings.helper.shadow_html).find('#shadow').fadeIn();
         
    }
 
  }

  // defines the function to hide the shadow
  $.fn.hide_shadow = function() {
 
    // fade it out, and then remove it from the dom
    $('#shadow').fadeOut('slow', function() {
      $(this).remove();
    });
 
  }
 
})(jQuery);

Now, when I click on the link the page fades to black and the user cannot click on anything. After the 3 second delay (defined in the AJAX callback), the black shadow fades away.

Shadow On

Syndicate content