Added the 'fallback', 'singlequoted', and 'doublequoted' attributes to Version 1.02 [29-Oct-2005] Added and pseudo-content. Allow debug id of -1 to mean "debug all posts" Version 1.01 [24-Sep-2005] Show debugging messages only for allowed-to-edit users. Version 1.00 [15-Sep-2005] Released */ $fbp_includes = array(); $fbp_vars = array(); $fbp_debug = FALSE; function fbp_process_include($level, $options_string, $file) { // // The include-options string can have both variable settings ("set // myvar = myval"), and built-in options (the same form, without // the 'set'). // // Variable settings are for the // // pseudo-content, and built-in options modify how the include is done // (currently, only "once" supported). // $options = array(); // built-in options we glean from $options_string global $fbp_debug; if ($fbp_debug) echo "[file-based-posts: include of '$file', option string '$options_string']\n
"; // // Parse the options string // $len = strlen($options_string); $pos = 0; // cursor which marches through $options_string while ($pos < $len) { // Look for any of: // // [set] name // [set] name = val // [set] name = 'val' // [set] name = "val" // // Allow a trailing comma // // 1 2 3 4 5 6 if (! preg_match('/\G(set\s+)?(\w+)\s*(=\s*(?:\'([^\']*)\'|\"([^\"]*)\"|(\S+))\s*)?(?:,\s*)?/', $options_string, $match, 0, $pos)) { echo "[bad include: ", $options_string, "]"; break; } $pos += strlen($match[0]); $name = $match[2]; $val = (strlen($match[3]) == 0) ? 1 : // [set] name ((strlen($match[4]) != 0) ? $match[4] : // [set] name = 'val' ((strlen($match[5]) != 0) ? $match[5] : // [set] name = "val" ((strlen($match[6]) != 0) ? $match[6] : // [set] name = val ""))); // [set] name = '' or [set] name = "" if (strlen($match[1]) == 0) { // an option, not a variable setting $options[$name] = $val; if ($fbp_debug) echo "[file-based-posts: include of $file, option '$name' has value '$val']\n
"; } else { // a variable setting global $fbp_vars; $fbp_vars[$name] = $val; if ($fbp_debug) echo "[file-based-posts: prior to include of $file, setting var '$name' to '$val']\n
"; } } // // Now, isolate all options that are allowed // $radio = $options['radio']; unset($options['radio']); $once = $options['once']; unset($options['once']); $raw = $options['raw']; unset($options['raw']); $cooked = $options['cooked']; unset($options['cooked']); $uncounted = $options['uncounted']; unset($options['uncounted']); // // Any options left now are unexpected. // if (count($options) > 0) { $attribs = join(", ", array_keys($options)); return "[file-based-posts: bad attributes to fbp_include: '$attribs']"; } $filetag = !is_null($radio) ? $radio : $file; global $fbp_includes; if (! $uncounted) $fbp_includes[$filetag]++; if ($once) { if ($fbp_includes[$filetag] > 1) return ""; } return fbp_process_one_file($level + 1, $file, array('cooked' => $cooked, 'raw' => $raw)); } function fbp_quote($val, $singlequoted, $doublequoted) { if ($singlequoted) return preg_replace("/'/", ''', $val); else if ($doublequoted) return preg_replace('/"/', '"', $val); else return $val; } function fbp_process_var($level, $options_string, $var_name) { // // First, parse the options string // $options = array(); // built-in options we glean from $options_string $len = strlen($options_string); $pos = 0; // cursor which marches through $options_string while ($pos < $len) { // Look for any of: // // name // name = val // name = 'val' // name = "val" // // Allow a trailing comma // // 1 2 3 4 5 if (! preg_match('/\G(\w+)\s*(=\s*(?:\'([^\']*)\'|\"([^\"]*)\"|(\S+))\s*)?(?:,\s*)?/', $options_string, $match, 0, $pos)) { echo "[bad var guts: ", $guts, "]"; break; } $pos += strlen($match[0]); $name = $match[1]; $val = (strlen($match[2]) == 0) ? 1 : // name ((strlen($match[3]) != 0) ? $match[3] : // name = 'val' ((strlen($match[4]) != 0) ? $match[4] : // name = "val" ((strlen($match[5]) != 0) ? $match[5] : // name = val ""))); // name = '' or [set] name = "" $options[$name] = $val; } // // Now, isolate all options that are allowed // $default = $options['default']; unset($options['default']); $fallback = $options['fallback']; unset($options['fallback']); $singlequoted = $options['singlequoted']; unset($options['singlequoted']); $doublequoted = $options['doublequoted']; unset($options['doublequoted']); // // Any options left now are unexpected. // if (count($options) > 0) { $attribs = join(", ", array_keys($options)); return "[file-based-posts: bad attributes to fbp_vars: '$attribs']"; } // // If the variable has been set, return its value. // global $fbp_vars; if (! is_null($fbp_vars[$var_name])) { if ($fbp_debug) echo "[file-based-posts: request for var '$var_name' returns value '$fbp_vars[$var_name]']\n
"; return fbp_quote($fbp_vars[$var_name], $singlequoted, $doublequoted); } // // If a fallback variable or list of variables is given, look for // one that's not null // if (! empty($fallback)) { $varlist = explode(',', $fallback); for ($i = 0; $i < count($varlist); $i++) { if (! is_null($fbp_vars[$varlist[$i]])) { $val = $fbp_vars[$varlist[$i]]; if ($fbp_debug) echo "[file-based-posts: request for var '$var_name' falls back to variable '$varlist[$i]' and returns value '$val']\n
"; return fbp_quote($val, $singlequoted, $doublequoted); } } } // return the default, if one given if (!is_null($default)) return fbp_quote($default, $singlequoted, $doublequoted); // the "default" default is an empty string return ""; } function fbp_process_inline_directive($level, $type, $guts) { global $fbp_debug; if ($type == 'include') { // just to be safe if ($level > 8) return "[file-based-posts: include level too deep]"; // basic form is "[options]: filename" if (preg_match('/^(?:(.*\S)\s*)?:\s*(\S+)\s*$/s', $guts, $match)) { global $fbp_vars; $hold = $fbp_vars; $retval = fbp_process_include($level + 1, $match[1], $match[2]); $fbp_vars = $hold; return $retval; } else return "[file-based-posts: bad include guts: $guts]"; } if ($type == 'var') { // basic form is "[options]: varname" if (preg_match('/^(?:(.*\S)\s*)?:\s*(\S+)\s*$/s', $guts, $match)) { return fbp_process_var($level, $match[1], $match[2]); } else return "[file-based-posts: bad var guts: $guts]"; } // this should never happen... return "[file-based-posts: unknown directive type '$type']"; } function fbp_process_one_file($level, $name, $options) { /* build the absolute file name */ $file_name = get_option('fbp_base_dir') . DIRECTORY_SEPARATOR . $name; global $fbp_debug; if ($fbp_debug) echo "[file-based-posts: fbp_process_one_file(level is $level, file = \"$file_name\")]\n
"; /* * Try to open the file, but be silent if it can't be done. */ @ $fp = fopen($file_name, 'r'); if (! $fp) { /* * If we can't open the file, just return the $content * that was passed in. */ if ($fbp_debug) { echo "[file-based-posts: can not open file "; if (isset($php_errormsg)) echo ": $php_errormsg; "; else echo "; turn on PHP's track_errors config to display file-open error message; "; echo "]\n

"; } #throw new Exception("bad fbp"); } /* * Go ahead and read the entire file. */ $new_content = fread($fp, filesize($file_name)); fclose($fp); if ($fbp_debug) echo "[file-based-posts: read " . strlen($new_content) . " bytes]\n
"; if ($options['raw']) { // leave it just as read from the file } else if ($options['cooked']) { $new_content = preg_replace('/&/', '&', $new_content); $new_content = preg_replace('//', '>', $new_content); } else { /* * To allow certain stuff to be in the file but not be part of the * post, we look for and remove * .... * sequences. */ $new_content = preg_replace('!.*?!s', '', $new_content); /* * Process directives */ $new_content = preg_replace_callback('/]*)>/', create_function('$match', 'return fbp_process_inline_directive($level, $match[1], $match[2]);'), $new_content); } if ($fbp_debug) echo "[file-based-posts: new content for level $level is " . strlen($new_content) . " bytes]\n
"; return $new_content; } function fbp_content_from_file($content) { global $id; /* * $fbp_debug is true if this post's $id is the id mentioned in the * fbp_debug_id option. Normally, this option is false (no debugging). */ global $fbp_debug; $fbp_debug = FALSE; if (get_option('fbp_debug_id') and $id and ($id == get_option('fbp_debug_id') or get_option('fbp_debug_id') < 0)) { global $user_ID; get_currentuserinfo(); /* only approved users should see debuggin messages */ $fbp_debug = user_can_edit_post($user_ID, $id); } if ($fbp_debug) { echo "[file-based-posts: about to inspect post #$id data in WordPress database]\n
"; $tmp = htmlspecialchars(substr($content, 0, 50)); $tmp = preg_replace('/ /', '·', $tmp); $tmp = preg_replace('/\t/', '\t', $tmp); $tmp = preg_replace('/\n/', '\n', $tmp); $tmp = preg_replace('/\r/', '\r', $tmp); /* should look at anything unprintable here.... */ echo "[file-based-posts: data begins: $tmp
\n"; } /* * If a post's $content's first line is: * FILE: filename.html * we ignore the rest of the $content and use what we read from the * file as the post $content. * * The filename must end with ".html", and may not contain a slash * or whitespace. * * All files are expected to be in the directory named in * the 'fbp_base_dir' option */ if (! preg_match('!^FILE:\s*([^\s\\/]+\.html)[ \t]*\r?($|\n)!', $content, $matches)) { /* * If this post is not for us, we simply return the * $content that was passed in to us. */ if ($fbp_debug) echo "[file-based-posts does not apply: content does not start with FILE: name.html]\n

"; return $content; } /* * Okay, this is a post that we need to deal with. */ $new_content = fbp_process_one_file(1, $matches[1], array()); /* try { } catch (Exception $e) { if (strlen($content) > strlen($matches[0]) + 30) echo "using previously-cached body content"; else echo "no content to show"; return $content; } /* */ if (! $id) { /* * I don't know that this is possible, but just to be sure: * if we don't know this post's $id then we can't do any * update, so just return the new content right away. */ if ($fbp_debug) echo "[file-based-posts: don't know post ID]\n

"; return $new_content; } global $wpdb; if (! $wpdb) { /* * If we don't have the database handle, then we can't do * any update, so just return the new content right away. */ if ($fbp_debug) echo "[file-based-posts: can't contact DB]\n

"; return $new_content; } /* * We'll compare what *should* be in the DB with what *is* in the * DB, and update the DB if needed. The only reason we care about * the updated content being in the DB is so that the site search * works (and, well, so that we have a saved version if the file * should suddenly disappear, I suppose). * * For the same reason (site-search working), we'll add the "don't * edit" comment with a space between letters, so that we * effectively add no words to the post, words that might otherwise * get picked up by the search. */ $db_content = $matches[0] . " \n" . $new_content; if ($db_content != $content) { /* what should be there is diff from what is there */ $new = addslashes($db_content); $wpdb->query("UPDATE $wpdb->posts SET post_content = '$new' WHERE ID = '$id'"); if ($fbp_debug) echo "[file-based-posts: detected file change; updated WordPress database]\n

"; } elseif ($fbp_debug) { /* what should be there is already there */ echo "[file-based-posts: content has not changed; leaving WordPress database as is]\n

"; } return $new_content; } /* * Function to display (and, upon submission, process) the plugin options */ function fbp_options_page() { if (isset($_POST['fbp_base_dir'])) { /* * User is attempting to update the base directory option */ $dir = $_POST['fbp_base_dir']; if (! preg_match('!^/!', $dir) and ! preg_match('!^[a-zA-Z:\\\\]!', $dir)) echo "

The base directory must be an absolute path

"; elseif (! @($D = opendir($dir))) echo "

The specified base directory “$dir” does not seem to exist (or is not readable by the web server); ignoring.

"; else { closedir($D); update_option('fbp_base_dir', $_POST['fbp_base_dir']); } } if (isset($_POST['fbp_debug_id'])) { /* * User is attempting to update the "debug-this-post" option */ $id = $_POST['fbp_debug_id']; if (preg_match('/^-?\d+$/', $id)) update_option('fbp_debug_id', $id); else echo "

Expected a number for the debug id

"; } ?>

Options for the "File-Based Posts" Plugin

The 'file-based posts' plugin allows you to maintain the body of a post in a file on the local server machine. Changes to the file are reflected immediately on the site.

Once you've set the base directory (below), you can create a file in it, say, "my-trip-to-the-store.html" (the file must end with ".html"), and put the body content of your post.

You then use the normal WordPress mechanism to create a post, but instead of typing the post body content in the big <textentry> box, enter only "FILE:" followed by the filename ("my-trip-to-the-store.html") and a newline. For example,

FILE: my-trip-to-the-store.html

 

Be sure to fill in the title, date, visibility, etc., on the normal WordPress "Write Post" page. Only the body content is in the file.
 

See the plugin's home page for more information.
 

Configuration Items

  • :
          
    Note: if file-based posts have already been published, changing the directory will cause them to break unless you've already copied all the files to the new directory.

  • Does it seem broken? Do you see “FILE: ...” live on the site? If so, enter the post ID here, and you'll see at the top of that post messages from this plugin letting you (and all logged-in users who have permission to edit the post) know what's happening. Normal readers will not see the extra messages.

    Enter "0" (zero) here once you're done, to turn off all debugging.

    Post ID to debug :
    Normally "0" -- zero -- when not debugging; set to a negative number to debug all posts.