Entries Tagged 'Software' ↓
Aug 15th, 2008 | Atlanta, Marketing, Software, Technology, Web

Just an announcement that we are going to be discussing Why you MUST have a Twitter Strategy at Atlanta Web Entrepreneurs on August 21, 2008.
I’m going to present a short intro/overview to Twitter and then, god willing and the creek don’t rise, we plan to have two (2) video conferences, one from Triangle Tweetup and the other from a soon-to-be-announced Industry luminary with over 25,000 Twitter followers!
After the 8pm break we’ll have a roundtable-less discussion and Q&A led by our featured participants:
Anyone that wants to attend should first be sure to have a Twitter account and to follow atlantaweb. We’ll use that list as a roll call for the meeting and we’ll announce our special guest on the atlantaweb Twitter account by 6pm Wednsday August 20th.
For more details and to RSVP see go here.
Aug 2nd, 2008 | Programming, Software, Web
UPDATE (2011-04-15):
Since I wrote this post I’ve learned a tremendous amount about WordPress plugin development; so much so that I can’t overstate how much more I know today than when I wrote this post years ago. So, while the following post might be a novelty to read, I highly recommend that you don’t use this approach.
The only way that a RESTful approach to web services in WordPress would make sense to me is if a team of rockstar WordPress plugin developers were to create a fully fleshed-out extension to WordPress that offered a complete RESTful web service implementation including one that addressed all edge cases and security concerns; only then I would consider not defaulting to the non-RESTful approach WordPress uses for AJAX.
In summary I recommend not trying to swim upstream today and instead use the approach provided by WordPress. Who knows, maybe in the future there will be a viable method of doing RESTful web services in WordPress.
So I’ve got a project where I need to have a Flash component built in Flex to call to a WordPress blog and get information about it’s latest post. Should be no problem right? The easy way to do this would be to just write a "rest.php" file and brute-force all the setup that WordPress does but I thought it would be so much more valuable to implement this as a plug-in.
I figured that I’d just quickly learn how to build a WordPress plug-in and create one for exposing RESTful web services; after all with a year of programming Drupal modules WordPress’ plug-in API can’t be that hard, right? Well turns out it wasn’t that easy and I think I have run into a design limitation with WordPress and I’m beginning to wish I’d just taken the brute-force approach and said to hell with writing a plug-in.
Although I am not 100% certain, and I hope someone can point out that I’m just doing something wrong, it seems sadly like I’m pushing the edges of the WordPress API and exposing where it’s design falls short. By the way, I’m working with WordPress v2.5 because why upgrade mid-project when god knows if WordPress will release another in the remaining days before this project is done and I’ll just have to do again before deployment?
Here’s the details. I started writing a plug-in called "RESTful Services" with a goal of implementing URLs that behave in the following fashion; {format} could potentially be html, xhtml, json, xml, rss, atom, etc.:
- http://example.com/services.{format}
- Provide a list of RESTful services in specified {format}, defaults to html
- http://example.com/services/{service}.{format}/{data}?{params}
- Provide a RESTful service in specified {format}, defaults to html, with optional provided data and parameters.
But before I got all those options working I just wanted to service a page from my RESTful Services plugin where Content-Type: text/plain. I found this page that professes to explain how to hook into the URL routing and after a few fits and starts I can came up with the following code for my plugin that would indeed response to my http://example.com/services URL:
wp-content/plugins/restful-services/restful-web-services.php:
1
2
3
4
5
6
7
8
9
10
11
12
13
| add_action('init', 'restful_services_flush_rewrite_rules');
function restful_services_flush_rewrite_rules() {
global $wp_rewrite;
$wp_rewrite->flush_rules();
}
add_filter('generate_rewrite_rules', 'restful_services_add_rewrite_rules');
function restful_services_add_rewrite_rules( $wp_rewrite ) {
$new_rules = array(
'services' => 'wp-content/plugins/restful-services/rest.php',
);
$wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
} |
The problem with the above was that it wouldn’t call "wp-content/plugins/restful-services/rest.php"; it would simply continued to call "index.php" and display the home page! After literally hours and hours of debugging with my trusty PhpEd IDE & debugger I was able to find that the code on lines 737 & 738 of "wp-includes/query.php" told WordPress that my service was the home page! It is almost seems like the "generate_rewrite_rules" was implemented as "a good idea" yet no testing has ever been done on it because for the best I can tell it doesn’t work. (Note I’ve reformatted the code to multiple lines so that it is easier to read and does not extend past the right margin of my blog):
wp-includes/query.php:
if ( !( $this->is_singular
|| $this->is_archive
|| $this->is_search
|| $this->is_feed
|| $this->is_trackback
|| $this->is_404
|| $this->is_admin
|| $this->is_comments_popup ) )
$this->is_home = true; |
I could possibly hack it to get past this by setting one of those to "true", but none of them are really appropriate; there is nothing there quite like an "is_service" instance variable. Setting something like "this->is_singular" or "this->is_feed" might work but it could manifest incompatibility problems with other plugins or future versions of WordPress. Frankly it was rather disappointing to discover this because it tells me that WordPress has hard-coded all the potential scenarios and doesn’t really have a way around it. Seems to me there should really be a hook here and the type of pages should be allowed to be expanded by plugins rather than be hardcoded as only one of ’singular’, ‘archive’, ’search’, ‘feed’, … and ‘home.’
Anyway, where this manifests itself is "wp-includes/template-loader.php" file which I have included in it’s entirety below. It is on lines 24 and 25 where the template loader loaded the home page because it’s not possible to specify otherwise:
wp-includes/template-loader.php:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
| /**
* Loads the correct template based on the visitor's url
* @package WordPress
*/
if ( defined('WP_USE_THEMES') && constant('WP_USE_THEMES') ) {
do_action('template_redirect');
$is_home = is_home() ;
if ( is_robots() ) {
do_action('do_robots');
return;
} else if ( is_feed() ) {
do_feed();
return;
} else if ( is_trackback() ) {
include(ABSPATH . 'wp-trackback.php');
return;
} else if ( is_404() && $template = get_404_template() ) {
include($template);
return;
} else if ( is_search() && $template = get_search_template() ) {
include($template);
return;
} else if ( is_home() && $template = get_home_template() ) {
include($template);
return;
} else if ( is_attachment() && $template = get_attachment_template() ) {
remove_filter('the_content', 'prepend_attachment');
include($template);
return;
} else if ( is_single() && $template = get_single_template() ) {
include($template);
return;
} else if ( is_page() && $template = get_page_template() ) {
include($template);
return;
} else if ( is_category() && $template = get_category_template()) {
include($template);
return;
} else if ( is_tag() && $template = get_tag_template()) {
include($template);
return;
} else if ( is_tax() && $template = get_taxonomy_template()) {
include($template);
return;
} else if ( is_author() && $template = get_author_template() ) {
include($template);
return;
} else if ( is_date() && $template = get_date_template() ) {
include($template);
return;
} else if ( is_archive() && $template = get_archive_template() ) {
include($template);
return;
} else if ( is_comments_popup() && $template = get_comments_popup_template() ) {
include($template);
return;
} else if ( is_paged() && $template = get_paged_template() ) {
include($template);
return;
} else if ( file_exists(TEMPLATEPATH . "/index.php") ) {
include(TEMPLATEPATH . "/index.php");
return;
}
} else {
// Process feeds and trackbacks even if not using themes.
if ( is_robots() ) {
do_action('do_robots');
return;
} else if ( is_feed() ) {
do_feed();
return;
} else if ( is_trackback() ) {
include(ABSPATH . 'wp-trackback.php');
return;
}
} |
Still another problem in this puzzle is the $wp->send_headers() method shown being called here on line 293 of "wp-includes/classes.php":
wp-includes/classes.php:
290
291
292
293
294
295
296
297
298
| function main($query_args = '') {
$this->init();
$this->parse_request($query_args);
$this->send_headers();
$this->query_posts();
$this->handle_404();
$this->register_globals();
do_action_ref_array('wp', array(&$this));
} |
The problem with the $wp->send_headers(), also from "wp-includes/classes.php", is that it seems to have the option of either serving an HTML content type on line 183 and 185, or a content type based on a feed (the content types for the feeds are set in their respective "wp-includes/feed-*.php" files) but no custom content types as far as I can determine as there seems to be no way to override calling this function or the logic path contained within:
wp-includes/classes.php:
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
| function send_headers() {
@header('X-Pingback: '. get_bloginfo('pingback_url'));
if ( is_user_logged_in() )
nocache_headers();
if ( !empty($this->query_vars['error']) && '404' == $this->query_vars['error'] ) {
status_header( 404 );
if ( !is_user_logged_in() )
nocache_headers();
@header('Content-Type: ' . get_option('html_type') . '; charset=' . get_option('blog_charset'));
} else if ( empty($this->query_vars['feed']) ) {
@header('Content-Type: ' . get_option('html_type') . '; charset=' . get_option('blog_charset'));
} else {
// We're showing a feed, so WP is indeed the only thing that last changed
if ( !empty($this->query_vars['withcomments'])
|| ( empty($this->query_vars['withoutcomments'])
&& ( !empty($this->query_vars['p'])
|| !empty($this->query_vars['name'])
|| !empty($this->query_vars['page_id'])
|| !empty($this->query_vars['pagename'])
|| !empty($this->query_vars['attachment'])
|| !empty($this->query_vars['attachment_id'])
)
)
)
$wp_last_modified = mysql2date('D, d M Y H:i:s', get_lastcommentmodified('GMT'), 0).' GMT';
else
$wp_last_modified = mysql2date('D, d M Y H:i:s', get_lastpostmodified('GMT'), 0).' GMT';
$wp_etag = '"' . md5($wp_last_modified) . '"';
@header("Last-Modified: $wp_last_modified");
@header("ETag: $wp_etag");
// Support for Conditional GET
if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
$client_etag = stripslashes(stripslashes($_SERVER['HTTP_IF_NONE_MATCH']));
else $client_etag = false;
$client_last_modified = empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? '' : trim($_SERVER['HTTP_IF_MODIFIED_SINCE']);
// If string is empty, return 0. If not, attempt to parse into a timestamp
$client_modified_timestamp = $client_last_modified ? strtotime($client_last_modified) : 0;
// Make a timestamp for our most recent modification...
$wp_modified_timestamp = strtotime($wp_last_modified);
if ( ($client_last_modified && $client_etag) ?
(($client_modified_timestamp >= $wp_modified_timestamp) && ($client_etag == $wp_etag)) :
(($client_modified_timestamp >= $wp_modified_timestamp) || ($client_etag == $wp_etag)) ) {
status_header( 304 );
exit;
}
}
do_action_ref_array('send_headers', array(&$this));
} |
Still, I was able to come up with a solution although it is so very hackish. My solution was to hook the "template_redirect" action on line 7 of "wp-includes/template-loader.php" (see code from that file above.) Though it seems to works thus far, my solution just feels wrong for the following reasons:
- It ignores the fact that WordPress continues to think that my web service URL is the home page,
- It first lets "$wp->send_headers()" set the content type before it overrides it,
- It uses an "exit" rather than a return to keep WordPress from serving up the home page template, and
- It doesn’t use the routing mechanism apparent built into WordPress (see "null" on line 30 of "wp-content/plugins/restful-services/restful-web-services.php" below, I assume it should have been the URL of the .php file I plan to execute but WordPress doesn’t see to use what I put there.)
The function "restful_web_services_exec_service()" is what is called to execute the appropriate web service:
wp-content/plugins/restful-services/restful-web-services.php:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| /*
Plugin Name: RESTful Web Services
Plugin URI: http://mikeschinkel.com/wordpress/restful-web-services/
Description: This plugin enables REST-based API web services for WordPress.
Author: Mike Schinkel
Version: 0.1
Author URI: http://mikeschinkel.com/
*/
define('RESTFUL_WEB_SERVICES_DIR', dirname(__FILE__));
define('RESTFUL_WEB_SERVICES_URL_PATTERN','services(/?.*)?');
$abspath = trim(str_replace('\\','/',ABSPATH),'/');
$rest_services_dir = str_replace('\\','/',RESTFUL_WEB_SERVICES_DIR);
$rest_services_path = trim(str_replace($abspath,'',$rest_services_dir),'/');
define('RESTFUL_WEB_SERVICES_PATH', $rest_services_path);
// NOTE, See: http://codex.wordpress.org/Custom_Queries#Permalinks_for_Custom_Archives
add_action('init', 'restful_web_services_flush_rewrite_rules');
add_filter('generate_rewrite_rules', 'restful_web_services_add_rewrite_rules');
add_action('template_redirect', 'restful_web_services_exec_service');
add_action('init', 'restful_web_services_flush_rewrite_rules');
function restful_web_services_flush_rewrite_rules() {
global $wp_rewrite;
$wp_rewrite->flush_rules();
}
add_filter('generate_rewrite_rules', 'restful_web_services_add_rewrite_rules');
function restful_web_services_add_rewrite_rules( $wp_rewrite ) {
$new_rules = array(
RESTFUL_WEB_SERVICES_URL_PATTERN => null,
);
$wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
}
function restful_web_services_exec_service() {
global $wp;
if ($wp->matched_rule==RESTFUL_WEB_SERVICES_URL_PATTERN) {
if ($wp->request == 'services') {
header('Content-Type: text/plain');
print 'TODO: Generate a list of RESTful Web Services here for this WordPress Blog.';
} else {
list($dummy,$service_name) = explode('/',$wp->request);
if (file_exists($service_php = (RESTFUL_WEB_SERVICES_DIR . '/services/' . $service_name . '.php'))) {
include_once $service_php;
} else {
header('Content-Type: text/plain');
status_header(404);
print '404 - Service not found.';
}
}
exit;
}
}
|
You’ll note that my function "restful_web_services_exec_service()" is very bare-bones at the moment serving only a plain text message "TODO:" for the path http://example.com/services, and assuming that any path http://example.com/services/{service} will execute a same-named .php file in the services subdirectory, i.e. for http://example.com/services/latest-post it will look for "wp-content/plugins/restful-web-services/services/lastest-post.php" and then delegate all the work to that .php file.
wp-content/plugins/restful-services/services/latest-vidclip.php:
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| /*
Filename: latest-post.php
Service Name: Latest Post
*/
global $wp_query;
$post = $wp_query->post;
$file = get_attached_file($post->ID);
if (empty($file)) {
list($file)= get_enclosed($post->ID);
}
$charset = get_option('blog_charset');
header('Content-Type: text/xml; charset=' . get_option('blog_charset'), true);
$link = get_permalink($post->ID);
$html = < <<post></post><post id="{$post->ID}"> <video>$file</video> <link />$link </post> POST; print $html; |
Here is an example output returned by calling http://example.com/services/latest-post:
1
2
| < ?xml version="1.0" encoding="UTF-8"?>
<post id="2"> <video>http://videos example.org/video1.flv</video> <link />http://example.org/sample-post-1/ </post> |
While bare-bones, that will meet my needs for the moment since I really only have the need for one service that responds to the HTTP GET verb. However, I can see where I might end up fleshing this out and create lots of helper functions that would streamline the creation of web services for RESTful access to the entirety of the WordPress database. If I do so I’ll be happy to donate this plugin to the community. In the mean time if you’d like to use this for your own use feel free but caveat emptor. However, if you’d like to use this code to create a plugin to contribute to the community, please contact me for collaboration. Of course if you’d like to retain me to fully flesh it out for you, I’m always open to that too! :-)
Finally, if anyone who knows the WordPress API better than I do can tell me where I erred in my analysis, and I really hope I did, please let me know so I can architect this thing better. On the other hand, if I was spot-on in my analysis maybe this will help the WordPress team understand what needs to be done so they can empower WordPress to generate any arbitrary content type without having to resort to hacks or bypassing the WordPress index.php startup code.
Jul 18th, 2008 | Software, Web
FCKeditor ShowBlocks Feature
The other day I wrote about how I was using Dean’s FCKeditor Plugin for Wordpress to solve the problems with WordPress’ default TinyMCE editor eating my hand-coded HTML tags. Well Joe Banks left a great and detailed comment giving several tips including one about the ShowBlocks button. Unfortunately the default comment system in WordPress ate part of his comment so I couldn’t see what he was talking about and went exploring.
Well it turns out he was right; ShowBlocks is awesome! It shows the <p> tags, the <div> tags, and more. Check the screen shot above to see a larger version.
Joe Banks forgot to include his blog URL on his comment (or at least I think he forgot), but I was able to dig it up via a quick google.
Jul 15th, 2008 | Software, Technology, Web
Interesting. I guess my recognition of the similarity of WordPress’ new Revisions control to Mediawiki/Wikipedia was not unique. Andrew Hyde proposed Wikify, as a plugin for WordPress to allow readers to revise posts for facts, spelling errors, et. al. Great idea.
Of course it becaming popular would be a mixed blessing as we’d have yet another thing for which we’d need to manage spam. :-)
Jul 15th, 2008 | Software, Technology, Web
When I relaunched my blog site I went with v2.3 but did most of the work several months before I launched. When I had time to launch v2.5 was out but I decided not to delay launching again and just went with v2.3 with plans to upgrade to v2.5 as soon as I had time (I’m working with 2.5 on a client probject and it is much nicer.)
Tonight I saw that Douglas Karr had a nice simple visual upgrade tutorial to WordPress 2.5 so I was going to see if I could do it tomorrow. No sooner did I find his post than up popped a notice on Twitter about WordPress 2.6 (how will we ever keep up?!? :-) Looks like I’ll be killing two birds with one stone, and hoping that the 2.6 upgrade will be as easy as Douglas made the 2.5 upgrade look.
Anyway, here’s a quick look at WordPress 2.6 from WordPress.org (note how it looks a lot like Mediawiki (Wikipedia) embedded into WordPress halfway through the vid):
Jun 13th, 2008 | Miscellaneous, Personal, Software, Web
It’s been a really long time since I last blogged, and it’s all because I got totally fed up with my old blog software and vowed never again to blog until I replaced it with WordPress. Well as you can guess getting around to replacing it took far longer than I planned, but now it is finally here! I’ve still have other non-blog related things that were housed at my domain I still need to fix such as this but now that the domain is switched over to WordPress I’ll have a bit more urgency to get those fixed. I look forward to rejoining to ranks of the blogging community.
What’s more, a lot has happened since I last blogged so I have lots of things to blog about in the coming weeks and months. Of course I have plenty of billable work that needs to get done so for all those of you who are waiting with baited breath for me to blog (LOL!), future blog posts won’t be coming as fast and furious as I’d like but at least with the new blog they can start to trickle out.
Aug 14th, 2007 | Atlanta, Software, Technology, Web
I’m at the Fox Theatre in my hometown of Atlanta today checking out the Adobe AIR Bus Tour Summer 07. It’s nice to be at the first event nationwide. I’m attending at the behest of a friend who thinks it going to be the "next big thing." I’m skeptical. I fear yet another proprietary attempt to empower developers to craft unique custom web interfaces to provide desktop functionality as a layer over web technologies, and that’s not a compliment. These types of things, especially when looking at the black box nature of opaque Flash SWF files, do their best to ignore those things that make the web work, i.e. stateless URL-addressed resources. The reality of Adobe AIR remains to be seen… P.S. It would have been nice if Adobe had consulted me to ensure that this event was more convenient for me. I mean, I actually had to leave my home and cross the street to attend. Adobe, Please! ‘-)
Jun 1st, 2007 | Software, Technology, Web
You gotta love that some at Microsoft actually have a sense of humor! From the PopFly FAQ (emphasis mine):
Why did you call it Popfly?
Well, left to our own devices we would have called it "Microsoft Visual Mashup Creator Express, May 2007 Community Tech Preview Internet Edition," but instead we asked some folks for help and they suggested some cool names and we all liked Popfly.
Feb 27th, 2007 | Opinion, Programming, Software, Web
Back in July of 2006 someone asked on the forum for ASPnix, the web host that specializes in CommunityServer, to add ISAPI Rewrite to their servers so that customers can clean up their URLs. Seven people including myself chimed in asked for it. Over the past eight months, little was said by ASPnix except by a former staffer who implied it was harm the stablity of their servers and who really gave no indication that any real consideration was being made to offer a solution for URL Rewriting.
Well finally, on Feb 22nd, Roma confirmed that ASPnix has will finally be offering ISAPI Rewrite on ASPnix’s web servers. That’s yet another IIS-centric web host who has finally freed its customers from the shackles of poorly designed URL Hell! Hooray!
Now let’s just hope that Scott Watermasysk can be convinced to add URL Rewriting support in CommunityServer using ISAPI Rewrite to eliminate .ASPX extensions and more on CommunityServer, sooner than later.
Feb 23rd, 2007 | Marketing, Opinion, Software, Web
Jon Udel is a big fan of using screencasts to instruct, and I’m a big fan of watching them when I want to learn something. I’d like to start doing some of my own. However, reading his post on screencasting tips today, I was reminded of how I can’t help but think that TechSmith is really missing out on a huge opportunity because of their pricing for Camtasia Studio.
I’ve followed them for a while, and I know that they are pretty much the gold standard for screen recording software. However, their price of $299 is in no-man’s land. It is too low for the market it currently targets, the corporate market, and too high for a much, much larger market; the amateur and semi-pro blogger.
For those company’s who need the software, TechSmith could easily double the price and would probably still sell 90% as many units. But of course, the lost 10% would be well more than made up for by the increased price per unit. And frankly, a higher price would motive resellers more (which, as a former reseller, I always hated that my business did better financially when I raised prices on customers.)
On the other hand, $299 is way past the threshold where an amateur bloggers would buy a copy. Frankly, I think that is the reason why we see so few screencasts on the web. In my 12+ years experience in selling software tools to developers, I’d say that $69 is probably about the right price for an amatuer to semi-pro blogger to say "Sure, what the heck, I’ll buy a copy and try this screencast thing."
TechSmith could easily cut feature features from this blogger version to differentiate from their professional version. For example, the blogger version could be limited to outputting only to Macromedia Flash, i.e. no AVI, Microsoft Windows Media, RealNetworks RealMedia and QuickTime. The could cut the output-to-EXE feature and the Create a CD-ROM feature. And probably a few more things.
But TechSmith would need to be extremely careful NOT to cut the features that bloggers would really need. I ran into this over and over with components vendors while running VBxtras/Xtras.Net. I’d suggest a lower-priced version so they could reach a slightly different market, and the vendor would want to cut so many features of the product that it would have been crippled. Instead what’s needed it to look at the features that are needed only by the high end customers and cut those while leaving feature every users could benefit from. For example, if TechSmith were to cut any of the recording, pre-production, or editing features they could very well end of with an expensive demo and lots of frustrated customers badmouthing them on the blogs.
But what they could do, given this market, is to have the screencast on the blogger edition end with a splash-screen/advertisement for Camtasia. Imagine that, having the ability to get advertisements on a larger percentage of the blogs on the web and the only thing requires would be to restructure an existing product! Can you say "No Brainer?"
So, what would this look like? I think if TechSmith were to offer two editions with the following prices they’d see a surge of new customers, the web would see an explosion of screencasts, and that would be great for (practically) everybody:
- $69 - Camtasia Studio, Express Edition
- $599 - Camtasia Studio, Professional Edition
So, if you are a blogger who thinks is a great idea and you’d be anxious to buy a copy of Camtasia Studio for $69 but wouldn’t even consider paying $299, why not go over to TechSmith’s website and send them some feedback on the subject. And be sure to point them to this URL so they can read my justification. Together, we can make a difference. :-)
P.S. One thing the skeptics in the audience should know is that I have recently started playing with the free software called Wink from DebugMode (thanks to Ben Coffey for the recommendation.) While it is great, I’d prefer the polish of Camtasia Studio. However, at $299 they won’t be getting a dime from me. On the other hand, for $69 I’d happy spend the money for the time and frustration it could hopefully save me, and I bet lots of other bloggers feel the same. So what will it be TechSmith: "$69 in revenue, or nothing?"