Jul 7

On my latest project, I was faced with a challenge: build Flash widgets that displayed dynamic data and could be embedded on any web page. Phase two of the widgets called for user interaction with the widget, as opposed to simply displaying content. It seemed that Flex would be the most logical technology to use for this project.

I used the Services and AMFPHP modules for Drupal to expose content via web services for my Flex widget to consume. However, I ran into a problem with the critical piece that Flex needs in order to get remote data. As a workaround to the browser security settings that prevent cross-site scripting, Adobe chose to implement an opt-in solution in the form of a file called crossdomain.xml. With crossdomain.xml, a site owner may allow a list of domains to read its data and the client, Flash in this case, is responsible for enforcement. As the business case for the project called for the widget to be embedded on any domain, I needed to use a promiscuous crossdomain policy, allowing access from all domains:

XML:
  1. <cross-domain-policy>
  2.   <allow-access-from domain="*"/>
  3. </cross-domain-policy>

Because of this policy, the widget was allowed to read all data on the site that the user has access to, including any (authenticated) content and (session) cookies. This could easily lead to privacy violations, account takeovers, theft of sensitive data, and bypassing of CSRF protections.

Other domains in a similar predicament have simply hosted their APIs on a different domain, thus preventing access to user data on the root domain. Unfortunately, the nature of Drupal doesn't allow for splitting the Services functionality out from the rest of the platform.

My solution was to write a forwarder, which is just a really simple PHP script, that is hosted on a subdomain. This subdomain hosts the promiscuous crossdomain policy, and the policy file on the root domain is configured to only accept requests from the subdomain. The forwarder does two things: it first makes sure that only requests to /services/amfphp are allowed; if allowed, the $HTTP_RAW_POST_DATA is sent to the root domain via cURL.

PHP:
  1. <?php
  2. // Configuration variables
  3. $server = 'http://mydomain.com/services/amfphp';
  4.  
  5. $request_uri = trim($_SERVER['REQUEST_URI']);
  6. if ($request_uri[0] === '/'){
  7.   $request_uri = substr($request_uri, 1);
  8. }
  9.  
  10. // Split the uri into components
  11. list($handler, $protocol) = split('/', $request_uri);
  12.  
  13. // Filter out unwanted requests
  14. if(($handler != 'services') && ($protocol != 'amfphp')){
  15.   exit();
  16. }
  17.  
  18. // Handle the post from the flash/flex client
  19. $xml = $HTTP_RAW_POST_DATA;
  20. if(strlen($xml) == 0){
  21.   exit();
  22. }
  23.  
  24. // Set the headers
  25. $header[] = "Content-type: text/xml";
  26. $header[] = "Content-length: ".strlen($xml);
  27.  
  28. $ch = curl_init($server);
  29. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  30. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  31. curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
  32. curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
  33.  
  34. $response = curl_exec($ch);
  35.  
  36. if (curl_errno($ch)) {
  37.   exit();
  38. }
  39. else {
  40.   curl_close($ch);
  41.   if(strlen($response) == 0){
  42.     exit();
  43.   }
  44.   print $response;
  45. }
  46.  
  47. exit();
  48.  
  49. ?>

Because the subdomain doesn't host any data at all, the security risk has been removed. If you are working with Drupal and Flash remoting, you'll need to consider the risks associated with promiscuous crossdomain policy files. While my solution certainly isn't the only solution, it is pretty effective and simple.

tags: , , , , , , , , , , ,

Jun 15

My niece, Brynn Doughty, played a wonderful piano piece for a recital on June 12, 2008.


Apr 3

drupal1.pngDrupal is incredibly flexible, but in current versions, lacks the ability to export content easily in the form of widgets. However, the Services module gives you that flexibility in a very easy to use manner.

Services allows you to expose pieces of your Drupal site, such as user, node, and views methods. Combine this with the integration of AMFPHP, you can build some extremely fast Flash and Flex widgets that display dynamic data and can be embedded on any website.

However, there is a catch.

I was working with another developer on a Facebook application that displayed my Flex widget, and it seems that in Facebook, you have to provide a static image for the user to click on in order to active the flash. This is clunky, and not the ideal solution for me.

Another option was to attempt to create cross-domain javascript widgets. I looked at JSON Server, which is a Services wrapper that returns data in JSON. This module worked great on the same domain, but failed on cross-domain calls. The reason why is that the module only accepts POST requests.

I ended up patching the module to accept GET requests structured in the following manner:

JavaScript:
  1. function get(){
  2.   headElement = document.getElementsByTagName("head").item(0);
  3.   var script = document.createElement("script");
  4.   script.setAttribute("type", "text/javascript");
  5.   // The callback parameter is needed so that the JSON gets returned correctly in order to be handled by the output function
  6.   script.setAttribute("src","http://server.com/services/json?method=views.getView&view_name=some_view&callback=display");
  7.   headElement.appendChild(script);
  8. }
  9.  
  10. function display(obj){
  11.   var theDiv = document.createElement("div");
  12.   theDiv.innerHTML = obj.data;
  13.   document.body.appendChild(theDiv);
  14. }

You'll note that I am doing script tag inserts into the head of the calling website. The script source is set to the path of my JSON server, with the services method as a parameter. The services method takes arguments, which differ depending on the service you are calling. In the case of the Views.getView service, you have to supply the name of the view to call.

The last parameter specifies the name of your output function. My patch takes this callback and wraps the JSON-formatted data returned by the service with the name of the callback. The callback is extremely important, as the JSON sent back by the server acts as a call to your defined callback function. From there, your callback function can display the data however it chooses.

Implementation of this method on a remote site is very easy:

JavaScript:
  1. <script type="text/javascript" src="http://server.com/path/to/javascript.js"></script>
  2. <script type="text/javascript">get();</script>

For my widget, I created a custom service that returned the output of a theme() function to render the widget template. This html output was then displayed by my JSON callback function.

Using the Services and (patched) JSON Server modules for Drupal, you can quite easily set up cross-domain javascript/html widgets that can be embedded anywhere.

tags: , , , , , , , , ,

Mar 4

Zend Studio For EclipseI have found PHP nirvana in a box.

I was trying to debug the lastest dev version of the userpoints module for Drupal, and was getting nowhere. The process of debugging PHP is tedious to begin with, but the practice of putting print statements into your code in places you think are likely the problem is a nightmare, and a huge black hole for productivity. Understanding what hooks are getting called and when they execute via print statements is a good way to waste an entire afternoon.

Enter Zend Studio. Their newest version is based on Eclipse, the open-source IDE that ties development in many languages into a single common UI. Zend Studio supports interactive debugging sessions with the server version of its debugger, and the client-server communication allows you to step into, over, and return from bits of code. It tells you what variables are currently in the stack, what the chain of functions is that is being called, and many other useful things.

I have to admit, I didn't come to this point of nirvana without trial. In trying to configure the Eclipse PDT to communicate effectively with the server debugger running on a remote server, I failed spectacularly. I tried a few other IDEs, and found them all to be unacceptable. I even tried Zend Studio 5.5. I was at the end of my patience when I found the newest version of Zend Studio, which. just. worked. Yes, I said it. It just worked, with the exception of changing the default port setting for the remote debugger.

My frustration is gone, I am at one with the code, and I can finally explain exactly what is going on in Drupal - how things get called, where and when they get called, and what the results of each call are.

And that bug with the userpoints module? The problem existed between keyboard and chair.

tags: , , , , , , , , , ,

Mar 3

I have the distinct pleasure of attending DrupalCon 2008, which is being held in Boston, MA this year. This year's attendance has blown out the attendance levels of previous DrupalCons, and it is a testament to the growing popularity of Drupal as a content management system.

The sessions I attended today were:

Design on the Edge of Drupal
This session was principally for designers, and talked about taking inspiration from architecture to build Drupal websites that are anything but standard. The point of the presentation was that a Drupal site does not have to look like a Drupal site, as long as users of the site have an expectation of how to navigate and get around.

Enterprise Theming in Drupal
As the engineering manager for Greenopolis, an environmentally-themed social networking site built 100% in Drupal, this session was very informative. We have thousands of nodes and users, and the purpose of this session was how to take a very large site and segment it thematically for the purposes of monetizing your audience.

Mapping Business Requirements to Drupal Modules
This session covered the basic problem of taking what the customer has asked you to build, and applying a development planning process for meeting those business goals.

Drupal Multimedia
As I continue to work on Greenopolis, one of the challenges I face is how to implement multimedia, especially in user-generated content. Performance and security are always major concerns, and this session provided a wealth of information regarding the options available to Drupal developers, as well as techniques for accomplishing some of this kind of functionality.

I will be attending DrupalCon on Tuesday and Wednesday, and post more information as the conference progresses.

Feb 14

scp3200atlantic_blue.jpgI finally broke down and bought a cellphone.

Nothing fancy, just a Sanyo SCP-3200 with Sprint as the carrier. There is nothing spectacular about this phone; it's got basic functionality, it's small and lightweight, and is a good solution for someone who doesn't like to talk on the phone in the first place.

Sadly, this phone had an unfortunate interaction with the inside of a toilet bowl, resulting in the white screen of death. Despite having been submerged in water for less than two seconds, the screen was completely shot, a victim of "massive water damage."

Fortunately, the screen was the only part of the phone that was damaged, but it could not be repaired, so I purchased a new phone of the same model. My new problem was how to get my contacts and other data off the old phone. Stupidly, I went to Radio Shack to purchase a data cable for $9.99. The data cable came with the HandsetManager software, which the manager at RadioShack said would help me retrieve my data from the broken cell phone.

Much to my dismay, the manager did not know the product he sold me, or else he would have known that the software does not support my phone. After searching the internet for a few hours, it became clear that a lot of people did not know how to connect their computer to the SCP-3200.

One tip led me to DataPilot, which is not free. Despite paying for a data cable and software, I had to shell out another $39.95 for the DataPilot software, which was risky considering the fact that I had no idea if it would work or not. Out of the box, this software did not support my phone. I had to update the software several times and from several different places before I could finally connect to my phone. Once I was able to do that, the software did a good job of reading the data from the phone, allowing me to edit the data and save it to my computer, and then send the data to the new cell phone.

lg_dp200-134.gifThe user interface is not the best, being neither intuitive nor visually appealing. However, it is very functional software, except it does not support ringtone editing on my phone, or photo editing. These pieces of missing functionality don't bother me though, as I use neither function on my phone.

So the bottom line is, if you need to connect to a Sanyo SCP-3200 cell phone, one solution is to purchase a data cable and DataPilot, and make sure you update the software after purchasing it.

tags: , , , , , , ,

Dec 13

I've been working on a Drupal module that generates a search form and presents the results below the form. However, I ran into a strange issue where the database query returned 3 records, and yet, the pager was displaying 9 pagination links.

The code that builds and handles the query looks like this:

PHP:
  1. <?php
  2. $query = 'select
  3.   n.nid,
  4.   n.title,
  5.   DATE_FORMAT(FROM_UNIXTIME(n.created), \'%c/%e/%Y\') as created,
  6.   c.field_product_price_value as price,
  7.   d.name,
  8.   t.tid
  9. from
  10.   {node} n
  11.   left join {node_revisions} r on r.vid = n.vid
  12.   left join {content_type_galleria_product} c on c.nid = n.nid
  13.   left join {term_node} t on t.nid = n.nid
  14.   left join {term_data} d on d.tid = t.tid ';
  15.  
  16. $groupby = ' group by n.nid';
  17. $clauses = array();
  18. $clauses[] = 'n.type = \'galleria_product\'';
  19.    
  20. foreach($form_values as $key => $value)
  21. {   
  22.   if(($key) && ($value != ''))
  23.   {
  24.     switch($key)
  25.     {
  26.       case 'name':
  27.         $clauses[] = 'n.title like \'%%'.db_escape_string($value).'%%\'';
  28.         break;
  29.       case 'price_range':
  30.         $clauses[] = 'c.field_product_price_value <= '.db_escape_string($value);
  31.         break;
  32.       case 'category':
  33.         $clauses[] = 't.tid = '.db_escape_string($value);
  34.         break;
  35.       case 'created':
  36.         switch($value)
  37.         {   
  38.           case 'today':
  39.             $clauses[] = 'DATE(FROM_UNIXTIME(n.created)) = CURDATE()';
  40.             break;
  41.           case 'current_week':
  42.             $clauses[] = 'WEEK(FROM_UNIXTIME(n.created)) = WEEK(NOW())';
  43.             break;
  44.           case 'current_month':
  45.            $clauses[] = 'MONTH(FROM_UNIXTIME(n.created)) = MONTH(NOW())';
  46.            break;
  47.          case 'current_year':
  48.            $clauses[] = 'YEAR(FROM_UNIXTIME(n.created)) = YEAR(NOW())';
  49.            break;
  50.        }
  51.        break;
  52.      default:
  53.        break;
  54.     }
  55.   }
  56. }
  57.    
  58. $limit = 30;
  59. $header = array(
  60.   array('data' => t('Name'), 'field' => 'n.title', 'sort' => 'asc'),
  61.   array('data' => t('Rating')),
  62.   array('data' => t('Price'), 'field' => 'c.field_product_price_value'),
  63.   array('data' => t('Category'), 'field' => 'd.name'),
  64.   array('data' => t('Created'), 'field' => 'n.created')
  65. );
  66.    
  67. $query .= (count($clauses) ? 'WHERE ' . implode(' AND ', $clauses) : '');
  68. $tablesort = tablesort_sql($header);
  69. $result = pager_query($query.$groupby.$tablesort, $limit, 0);
  70. $rows = array();
  71.  
  72. // Retrieve all the data found by the query
  73. while($data = db_fetch_array($result))
  74. {
  75.   $current_avg = votingapi_get_voting_results('node', $data['nid'], 'percent', 'vote', 'average');
  76.   $stars = variable_get('fivestar_stars_'. (!isset($node) ? 'default' : $node->type), 5);
  77.   $rows[] = array(
  78.     l($data['title'], 'node/'.$data['nid']),
  79.     theme('fivestar_static', $current_avg[count($current_avg)-1]->value, $stars),
  80.     '$'.$data['price'],
  81.     l($data['name'], 'taxonomy/term/'.$data['tid']),
  82.     $data['created'],
  83.   );
  84. }
  85.    
  86. if(empty($rows))
  87. {
  88.   $rows[] = array(array('data' => t('Your search failed to find any products.'), 'colspan' => 3));
  89. }
  90.  
  91. $output = theme('table', $header, $rows);
  92. $output .= theme('pager', null, $limit);
  93. ?>

After banging my head against the wall for a few hours, I came across a post by Kris Buytaert which explained why I was seeing this problem.

To summarize, the code that produces the pagination links is in pager.inc. Within this code is this bit:

PHP:
  1. <?php
  2.  
  3. $count_query = preg_replace(array('/SELECT.*?FROM /As', '/ORDER BY .*/'), array('SELECT COUNT(*) FROM ', ''),$query);
  4.  
  5. ?>

You'll note that the pattern matching is case insensitive. As my query was written all in lowercase, the pagination code was not matching anything. All I had to do was rewrite the query such that the MySQL keywords were in uppercase.

A bug, to be sure, considering that the SQL Standard does not call for specific case. As pager.inc is part of the Drupal core, hopefully it will get patched and fixed soon.

tags: , , ,

Oct 19

I recently built my first module for Drupal, which exposes data from the Userpoints module to Views. There was some talk with the CEO of my company about releasing the module to the community as a contributed module, and some hedging about whether to release it or not.

Releasing modules shouldn't even be a topic of discussion within a company. The work I did on this module was built off the many, many, many hours others have spent on Userpoints and Views. In addition, thousands of developers contributed to, and continue to contribute to Drupal. For a company to even consider taking an Open Source project, developing a feature on top of it and then not releasing it, is rude and insulting to all the developers that have work on this OSS project.

Companies who use Drupal and other open source software have directly benefited from the work of thousands. These companies have saved gobs and gobs of cash by stating with Drupal as a base and then building on top. That base was built by ordinary people, and that base depends upon people contributing their code.

Credit should be given where it is due and if your company has sponsored the development of Drupal modules by paying your salary, that should be mentioned. At the very least, your company's name can be in the module itself with the README file and within the code.

Again, this should not even be a topic of discussion. If you work with Drupal and extend its functionality, release the code to the community.

tags: , , , , , , , , ,

Oct 15

I have found that the core Drupal profile module provides very limited customization possibilities. However, the Usernode and Nodeprofile modules help out immensely with this.

If you want access to custom user profile fields, such as CCK fields, you simply load the profile:

PHP:
  1. <?php
  2. $usernode = nodeprofile_load('uprofile', $node->uid);
  3. ?>

tags: , , , , ,

Oct 11

Drupal is a fairly flexible system as far as CMS applications go, and is even more flexible as a development platform. The Views module gives developers ways to dynamically build queries of data, and display that data in many different ways. Sometimes, however, you want to display views in ways not supported through the admin interface. Fortunately, there are other ways to get the job done.

For example, I have a block in my sidebar where I want to display a list of the latest blog post, latest forum topic, and latest user poll. Each of these lists can be created as separate views. However, there doesn't seem to be any functionality, either in the Views module or another third-party module, that allows you to display multiple unrelated views in a single block. So, we turn to the code. The Views API provides a mechanism for building and displaying your views anywhere in your templates:

PHP:
  1. <?php views_get_view($name_of_view); ?>

and

PHP:
  1. <?php views_build_view($type, $view, $view_args, $use_pager, $node_limit); ?>

Clearly, the views_get_view() function retrieves the view definition from the database, while the views_build_view() function renders it.

Back to my example - I simply created a new block, and set the input format to PHP. Then, I used the following code to populate the block with the views:

PHP:
  1. <h2>Latest Blog Post:</h2>
  2. <?php  echo views_build_view('embed', views_get_view('latest_blogpost'), null, false, 1); ?>
  3. <h2>Latest Forum Topic:</h2>
  4. <?php echo views_build_view('embed', views_get_view('latest_forumtopic'), null, false, 1); ?>
  5. <h2>Latest Poll:</h2>
  6. <?php echo views_build_view('embed', views_get_view('latest_userpoll'), null, false, 1); ?>

The above code shows the rendered view. However, there are times where you want or need to do some additional processing on the values returned by the view. Fortunately, the Views module is incredibly powerful, and has a ton of options for retrieving the views.

In the above example, the first argument to the views_build_view() function is 'embed'. There are several other options:

  • page - Produces output as a page, sent through the theme. The only real difference between this and block is that a page uses drupal_set_title to change the page title.
  • block - Produces output as a block, sent through the theme
  • embed - Use this if you want to embed a view onto another page, and don't want any block or page specific things to happen to it.
  • result - Returns an array of information, including a database object that you can use db_fetch_object() on
  • items - Returns an array like resul, however, it contains an array of objects found by the queries, so you don't have to fetch the objects yourself.
  • queries - returns an array, summarizing the queries, but does not run them

This information comes directly from the code, and hopefully, it will be of some assistance to people who are looking for this kind of functionality.

tags: , , , , , ,

« Previous Entries