Skip to content
Browse files

0.4.3.3

git-svn-id: https://svn.wp-plugins.org/better-related/trunk@312962 b8457f37-d9ea-0310-8a92-e5e31aec5664
  • Loading branch information...
0 parents commit 54f535b233744ccd8505a5cd5226f571989d2c7a nkuttler committed
1 .gitignore
@@ -0,0 +1 @@
+log.txt
312 better-related.php
@@ -0,0 +1,312 @@
+<?php
+/*
+ Copyright 2010 Nicolas Kuttler (email : wp@nicolaskuttler.de )
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+Plugin Name: Better Related Content
+Author: Nicolas Kuttler
+Author URI: http://www.nkuttler.de/
+Plugin URI: http://www.nkuttler.de/wordpress-plugin/better-related-posts-and-custom-post-types/
+Description: Better related posts plugin that finds custom post types and searches custom taxonomies
+Version: 0.4.3.3
+Text Domain: better-related
+*/
+
+/**
+ * @package better-related
+ * @subpackage pluginwrapper
+ * @since 0.0.1
+ */
+if ( !class_exists( 'BetterRelated' ) ) {
+
+ class BetterRelated {
+
+ /**
+ * Array containing the options
+ *
+ * @since unknown
+ *
+ * @var array
+ */
+ private $options;
+
+ /**
+ * Path to the plugin
+ *
+ * @since 0.2.1
+ *
+ * @var string
+ */
+ protected $plugin_dir;
+
+ /**
+ * Path to the plugin file
+ *
+ * @since 0.2.3
+ *
+ * @var string
+ */
+ protected $plugin_file;
+
+ /**
+ * Plugin config version
+ *
+ * @since 0.2.5
+ *
+ * @var string
+ */
+ private $version = '0.4.2';
+
+ /**
+ * Constructor, set up the variables
+ *
+ * @since 0.0.1
+ *
+ * return none
+ */
+ protected function __construct() {
+ // Full path to main file
+ $this->plugin_file= __FILE__;
+ $this->plugin_dir = dirname( $this->plugin_file );
+ $this->options = $this->get_saved_options();
+ }
+
+ /**
+ * Return a specific option value
+ *
+ * @since 0.1.1
+ *
+ * @param string $option name of option to return
+ * @return mixed
+ */
+ protected function get_option( $option ) {
+ if ( isset ( $this->options[$option] ) )
+ return $this->options[$option];
+ else
+ return false;
+ }
+
+ /**
+ * Return the plugin options as stored in the DB, or an empty array
+ *
+ * @fixme don't fill empty strings with defaults! (?)
+ * @since 0.4.1
+ *
+ * @return array
+ */
+ protected function get_saved_options() {
+ if ( $options = get_option( 'better-related' ) )
+ return $options;
+ // If the option wasn't found, the plugin wasn't activated properly
+ $this->create_fulltext_index( 'posts', 'post_content' );
+ $this->create_fulltext_index( 'posts', 'post_title' );
+ return $this->defaults();
+ }
+
+ /**
+ * Override the options retrived from the db for custom displays etc
+ *
+ * @since unknown
+ *
+ * @param array config options
+ * @return success
+ */
+ protected function override_options( $options ) {
+ $db_options = $this->options;
+ if ( !is_array( $db_options ) || !is_array( $options ) ) {
+ trigger_error( 'Warning: override_options() did not receive a config array, or the stored config was invalid.' );
+ return false;
+ }
+ $this->options = array_merge( $db_options, $options );
+ return true;
+ }
+
+ /**
+ * Update a plugin option
+ *
+ * @since 0.3.5
+ *
+ * @return none
+ */
+ protected function update_option( $option, $value ) {
+ $this->options[$option] = $value;
+ update_option( 'better-related', $this->options );
+ }
+
+ /**
+ * Return default plugin configuration
+ *
+ * @since 0.2.3
+ *
+ * @return array config
+ */
+ protected function defaults() {
+ $config = array(
+ 'version' => $this->version,
+ 'autoshowpt' => array(
+ //'post'=> true
+ ),
+ 'usept' => array(
+ 'post' => true
+ ),
+ 'usetax' => array(),
+ 'autoshowrss' => false,
+ 'do_c2c' => 1,
+ 'do_t2t' => 0,
+ 'do_t2c' => 0,
+ 'do_k2c' => 0,
+ 'do_k2t' => 0,
+ 'do_x2x' => 1,
+ 'minscore' => 50,
+ 'maxresults' => 5,
+ 'cachetime' => 60,
+ 'filterpriority' => 10,
+ 'log' => false,
+ 'loglevel' => false,
+ 'storage' => 'postmeta',
+ 'storage_id' => 'better-related-',
+ 'querylimit' => 1000,
+ 't_querylimit' => 10000,
+ 'mtime' => time(),
+ 'relatedtitle' => sprintf(
+ "<strong>%s</strong>",
+ __( 'Related content:', 'better-related' )
+ ),
+ 'relatednone' => sprintf(
+ "<p>%s</p>",
+ __( 'No related content found.', 'better-related' )
+ ),
+ 'thanks' => 'below',
+ 'stylesheet' => true,
+ 'showdetails' => false
+ );
+ return $config;
+ }
+
+ /**
+ * Create a fulltext index on a column
+ *
+ * @since 0.3.2
+ * @todo first query always throws a WP_DEBUG error
+ *
+ * @return none
+ */
+ protected function create_fulltext_index( $table, $column ) {
+ if ( $this->fulltext_index_exists( $table, $column ) )
+ return;
+ global $wpdb;
+ $query = "CREATE FULLTEXT INDEX {$column}_index ON {$wpdb->prefix}{$table} ($column);";
+ $r = $wpdb->query( $wpdb->prepare( $query ) );
+ }
+
+ /**
+ * Check if a fulltext index for a column exists
+ *
+ * @since 0.3.2
+ *
+ * @return bool full text index exists
+ */
+ protected function fulltext_index_exists( $table, $column ) {
+ global $wpdb;
+ $query = "SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_NAME='{$wpdb->prefix}posts' AND COLUMN_NAME='$column' AND INDEX_TYPE='FULLTEXT';";
+ $indexes = $wpdb->get_var( $wpdb->prepare( $query ) );
+ if ( intval( $indexes ) > 1 ) {
+ trigger_error( sprintf( __( 'Warning: Multiple fulltext indexes found for %s', 'better-related' ), "{$wpdb->prefix}$table.$column" ) );
+ return true;
+ }
+ elseif ( $indexes === '0' )
+ return false;
+ elseif ( $indexes === '1' )
+ return true;
+ trigger_error( sprintf( __( 'Warning: Unknown mysql result from %s', 'better-related' ), $query ) );
+ return false;
+ }
+
+ /**
+ * Deactivate this plugin and die with an error message
+ *
+ * @since 0.2.8
+ *
+ * @param string error
+ * @return none
+ */
+ protected function deactivate_and_die( $error = false ) {
+ #load_plugin_textdomain(
+ # 'better-related',
+ # false,
+ # basename( $this->plugin_dir ) . '/translations'
+ #);
+ $message = sprintf( __( "Better Related has been automatically deactivated because of the following error: <strong>%s</strong>." ), $error );
+ if ( !function_exists( 'deactivate_plugins' ) )
+ include ( ABSPATH . 'wp-admin/includes/plugin.php' );
+ deactivate_plugins( __FILE__ );
+ wp_die( $message );
+ }
+
+ /**
+ * Log helper, exposes the log to the web by default
+ *
+ * @since 0.0.1
+ *
+ * @param string $msg message
+ * @param string $level loglevel
+ * @return none
+ */
+ protected function log( $msg, $level = 0 ) {
+ if ( !$this->get_option( 'log' ) )
+ return;
+ $msg = date( 'H:i:s' ) . ": $msg";
+ $msg .= "\n";
+ $log = $this->plugin_dir . '/log.txt';
+ $fh = fopen( $log, 'a' );
+ if ( !$fh ) {
+ trigger_error( sprintf(
+ __( "Logfile %s is not writable..", 'better-related' ),
+ $log
+ ) );
+ return;
+ }
+ if ( !$this->get_option( 'log' ) )
+ return;
+ $loglevel = $this->get_option( 'loglevel' );
+ if ( $loglevel != 'all' && $loglevel != $level && $level != 'global' )
+ return;
+ fwrite( $fh, $msg );
+ fclose( $fh );
+ }
+
+ }
+
+ /**
+ * Instantiate the appropriate classes
+ */
+ $missing = 'Core plugin files are missing, please reinstall the plugin';
+ if ( is_admin() ) {
+ if ( @include( 'inc/admin.php' ) )
+ $BetterRelatedAdmin = new BetterRelatedAdmin;
+ else
+ BetterRelated::deactivate_and_die( $missing );
+ }
+ else {
+ if ( @include( 'inc/frontend.php' ) ) {
+ global $BetterRelatedFrontend;
+ $BetterRelatedFrontend = new BetterRelatedFrontend;
+ }
+ else
+ BetterRelated::deactivate_and_die( $missing );
+ }
+
+}
88 css/admin.css
@@ -0,0 +1,88 @@
+/* version 0.2.3 */
+p, li {
+ max-width: 100ex;
+}
+p {
+ text-align: justify;
+}
+thead {
+ text-align: center;
+ font-weight: bold;
+}
+blockquote {
+ font-style: italic;
+ max-width: 80ex;
+}
+
+.wrap {
+ max-width: 2000px;
+}
+#nkcontent {
+ float: left;
+ width: 70%;
+}
+#nkbox {
+ width: 20%;
+ float: left;
+ background-color: #ddf;
+}
+.box,
+#nkbox {
+ margin: 16px 8px;
+ padding: 8px;
+ border-radius: 6px;
+ -moz-border-radius: 6px;
+ -webkit-border-radius: 6px;
+}
+#nkbox div {
+ text-align: right;
+ font-size: smaller;
+ clear: right;
+}
+#nkbox ul {
+ margin: 4px 0;
+}
+#nkbox ul li {
+ margin: 4px 0 0 4px;
+}
+#nkbox .gravatar {
+ float: right;
+ margin: 8px 0 0 8px;
+ width: 55px;
+ text-align: center;
+}
+
+.form-table span {
+ padding: 3px;
+ border: 1px solid #ccc;
+}
+
+.form-table-clearnone {
+ clear: none;
+ width: auto;
+}
+
+.nkthemeswitch-admin {
+ text-align: center;
+}
+a.nkthemeswitch {
+ line-height: 28px;
+ padding: 4px 4px;
+ background: #ddf;
+ color: #0d0d0d;
+ text-decoration: none;
+ font-size: 12px;
+ border: 1px solid #fff;
+ border-radius: 6px;
+ -moz-border-radius: 6px;
+ -webkit-border-radius: 6px;
+ white-space: pre;
+}
+a.nkthemeswitch:hover {
+ background: #eef;
+ border: 1px solid #ccf;
+}
+
+#zcmail * {
+ display: block;
+}
3 css/better-related.css
@@ -0,0 +1,3 @@
+.betterrelated .score {
+ font-size: xx-small !important;
+}
668 inc/admin.php
@@ -0,0 +1,668 @@
+<?php
+
+/**
+ * @package better-related
+ * @subpackage admin
+ * @since 0.1.1
+ * @todo manual scoring metabox
+ */
+class BetterRelatedAdmin extends BetterRelated {
+
+ /**
+ * Constructor, set up the admin interface
+ *
+ * @since 0.1.1
+ *
+ * @return none
+ */
+ public function __construct() {
+ BetterRelated::__construct();
+ load_plugin_textdomain(
+ 'better-related',
+ false,
+ basename( $this->plugin_dir ) . '/translations'
+ );
+ add_action(
+ 'admin_menu',
+ array ( $this, 'add_page' )
+ );
+ add_action(
+ 'admin_init',
+ array ( $this, 'register_setting' )
+ );
+ register_activation_hook(
+ $this->plugin_file,
+ array( $this, 'activate' )
+ );
+ add_action(
+ 'wp_insert_post',
+ array( $this, 'timestamp_content' )
+ );
+ }
+
+ /**
+ * Activation hook
+ *
+ * Get the default settings and update the plugins settings with new
+ * default values. Take care not to overwrite empty strings or falses
+ * in the user's options.
+ *
+ * @since 0.2.3
+ *
+ * @return none
+ */
+ public function activate() {
+ //$mysql = $this->get_mysql_version();
+ //if ( !version_compare( $mysql, 5, '>=' ) )
+ // $this->deactivate_and_die( 'Your MySQL version is not supported' );
+ $defaults = $this->defaults();
+ $saved = $this->get_saved_options();
+ foreach( $defaults as $key => $value ) {
+ if ( @$saved[$key] === '' )
+ unset( $defaults[$key] );
+ elseif ( @$saved[$key] === false )
+ unset( $defaults[$key] );
+ }
+ update_option( 'better-related', array_merge( $defaults, $saved ) );
+ $this->create_fulltext_index( 'posts', 'post_content' );
+ $this->create_fulltext_index( 'posts', 'post_title' );
+ }
+
+ /**
+ * Get the MySQL version string
+ *
+ * @since 0.4.3.1
+ *
+ * @return string MySQL version
+ */
+ private function get_mysql_version() {
+ global $wpdb;
+ $query = "SELECT VERSION();";
+ $r = $wpdb->get_var( $wpdb->prepare( $query ) );
+ return $r;
+ }
+
+ /**
+ * Check if fulltext indexes exist and print an error
+ *
+ * @since 0.3.2
+ *
+ * @return none
+ */
+ private function fulltext_index_exists_admin( $table, $column ) {
+ global $wpdb;
+ if ( !$this->fulltext_index_exists( $table, $column ) ) {
+ echo '<div class="error">';
+ printf(
+ __( "There is no fulltext index for %s", 'better-related' ),
+ "{$wpdb->prefix}$table.$column"
+ );
+ echo '</div>';
+ }
+ }
+
+ /**
+ * Set up the options page
+ *
+ * @since 0.1.1
+ *
+ * @return none
+ */
+ public function add_page() {
+ if ( current_user_can ( 'manage_options' ) ) {
+ $options_page = add_options_page (
+ __( 'Better Related Content' , 'better-related' ),
+ __( 'Better Related Content' , 'better-related' ),
+ 'manage_options',
+ 'better-related',
+ array ( $this , 'admin_page' )
+ );
+ add_action(
+ 'admin_print_styles-' . $options_page,
+ array( $this, 'css' )
+ );
+ }
+ }
+
+ /**
+ * Load admin CSS style
+ *
+ * @since 0.2.4
+ *
+ * @return none
+ */
+ public function css() {
+ wp_register_style(
+ 'better-related',
+ plugins_url( basename( $this->plugin_dir ) . '/css/admin.css' ),
+ null,
+ '0.0.1'
+ );
+ wp_enqueue_style( 'better-related' );
+ }
+
+ /**
+ * Register the plugin option with the setting API
+ *
+ * @since 0.1.1
+ *
+ * @return none
+ */
+ public function register_setting() {
+ register_setting(
+ 'better-related_options',
+ 'better-related',
+ array( $this, 'sanitize_options' )
+ );
+ }
+
+ /**
+ * Sanitize the options
+ *
+ * @since 0.3.1
+ *
+ * @return sanitized data
+ */
+ public function sanitize_options( $data ) {
+ $defaults = $this->defaults();
+ // float
+ $float = array(
+ 'do_c2c',
+ 'do_t2t',
+ 'do_t2c',
+ 'do_k2c',
+ 'do_k2t',
+ 'do_x2x',
+ 'minscore'
+ );
+ foreach ( $float as $var ) {
+ $data[$var] = floatval( $data[$var] );
+ }
+ // int
+ $int = array(
+ 'maxresults',
+ 'cachetime',
+ 'filterpriority',
+ 'querylimit',
+ 't_querylimit'
+ );
+ foreach ( $int as $var ) {
+ $data[$var] = intval( $data[$var] );
+ }
+ // int bigger zero
+ $int_gt_zero = array(
+ 'cachetime',
+ 'filterpriority',
+ 'querylimit',
+ 't_querylimit'
+ );
+ foreach ( $int_gt_zero as $var ) {
+ if ( $data[$var] < 1 ) {
+ $data[$var] = $defaults[$var];
+ }
+ }
+ // string, not empty
+ $not_empty = array(
+ 'storage_id'
+ );
+ foreach ( $not_empty as $var ) {
+ if ( !$data[$var] ) {
+ $data[$var] = $defaults[$var];
+ }
+ }
+ // lower limit
+ if ( $data['querylimit'] <= 100 )
+ $data['querylimit'] = 100;
+ // bigger than var
+ if ( $data['querylimit'] >= $data['t_querylimit'] )
+ $data['t_querylimit'] = $data['querylimit'] * 2;
+ // boolean
+ $bool = array(
+ 'autoshowrss',
+ 'log',
+ 'stylesheet',
+ 'showdetails'
+ );
+ foreach ( $bool as $var ) {
+ if ( $data[$var] == 'on' )
+ $data[$var] = true;
+ else
+ $data[$var] = false;
+ }
+ // @todo usept and showpt
+ // update mtime if necessary
+ $update_mtime = array(
+ 'usept',
+ 'usetax',
+ 'do_c2c',
+ 'do_t2t',
+ 'do_t2c',
+ 'do_k2c',
+ 'do_k2t',
+ 'do_x2x',
+ 'querylimit',
+ 't_querylimit'
+ );
+ foreach ( $update_mtime as $var )
+ if ( @$data[$var] != $this->get_option( $var ) )
+ $data['mtime'] = time();
+ return $data;
+ }
+
+ /**
+ * Form input helper that produces the correct HTML markup
+ *
+ * @since 0.2.2
+ *
+ * @param string $label Input label
+ * @param string $name Input name
+ * @param string $comment Input comment
+ * @return none
+ */
+ private function input( $label, $name, $comment = false, $size = false ) {
+ if ( is_integer( $size ) )
+ $size = " size=\"$size\" "; ?>
+ <tr valign="top">
+ <th scope="row"> <?php
+ echo $label; ?>
+ </th>
+ <td> <?php
+ echo '<input type="text" name="better-related[' . $name . ']" value="' . $this->get_option( $name ) . '"' . $size . '/>';
+ if ( $comment )
+ echo ' ' . $comment;
+ ?>
+ </td>
+ </tr> <?php
+ }
+
+ /**
+ * Form hidden input helper
+ *
+ * @since 0.3.8
+ *
+ * @param string $name Input name
+ * @return none
+ */
+ private function hidden( $name ) {
+ echo '<input type="hidden" name="better-related[' . $name . ']" value="' . $this->get_option( $name ) . '" />';
+ }
+
+ /**
+ * Form checkbox helper
+ *
+ * @since 0.2
+ * @todo this is messy
+ *
+ * @param string $label Input label
+ * @param mixed $name String option name or array of name => [ subnames ]
+ * @param string $comment Input comment
+ * @return none
+ */
+ private function checkbox( $label, $name, $comment = false ) { ?>
+ <tr valign="top">
+ <th scope="row"> <?php
+ echo $label; ?>
+ </th>
+ <td> <?php
+ if ( is_array( $name ) ) {
+ foreach( $name[1] as $value ) {
+ $this->single_checkbox( $name[0], $value );
+ }
+ }
+ else {
+ $checked = '';
+ if ( $this->get_option( $name ) )
+ $checked = ' checked="checked" ';
+ echo '<input ' . $checked . 'name="better-related[' . $name . ']" type="checkbox" />';
+ }
+ if ( $comment )
+ echo " $comment";
+ ?>
+ </td>
+ </tr> <?php
+ }
+
+ /**
+ * Form checkbox helper, for a single checkbox when an array of boxes
+ * was passed to checkbox()
+ *
+ * @since 0.2.2
+ *
+ * @param string $name Checkbox name
+ * @param string $value Checkbox value
+ * @return none
+ */
+ private function single_checkbox( $name, $value ) {
+ $option = $this->get_option( $name );
+ $checked = '';
+ if ( isset( $option[$value] ) && $option[$value] )
+ $checked = ' checked="checked" ';
+ echo "<span><input $checked type=\"checkbox\" name=\"better-related[" . $name . '][' . $value . "]\" /> $value</span>" . "\n";
+ }
+
+ /**
+ * Form select helper
+ *
+ * @since 0.2
+ *
+ * @param string $label Label
+ * @param string $name Select name
+ * @param array $choices Possible choices (strings)
+ * @return none
+ */
+ private function select( $label, $name, $choices, $comment = false ) {
+ $current = $this->get_option( $name ); ?>
+ <tr valign="top">
+ <th scope="row"> <?php
+ echo $label; ?>
+ </th>
+ <td>
+ <select name="better-related[<?php echo $name ?>]"> <?php
+ foreach( $choices as $key => $value ) {
+ if ( $key == $current )
+ $select = ' selected="selected" ';
+ else
+ $select = '';
+ echo "<option $select value=\"$key\" >$value</option>\n";
+ } ?>
+ </select> <?php
+ if ( $comment )
+ echo ' ' . $comment;
+ ?>
+ </td>
+ </tr> <?php
+ }
+
+ /**
+ * Output the options page
+ *
+ * @todo manual scoring options
+ * @since 0.1.1
+ *
+ * @return none
+ */
+ public function admin_page () {
+ $this->fulltext_index_exists_admin( 'posts', 'post_content' );
+ $this->fulltext_index_exists_admin( 'posts', 'post_title' ); ?>
+ <div id="nkuttler" class="wrap" >
+ <div id="nkcontent">
+ <h2><?php _e( 'Better Related Posts and Content Options', 'better-related' ) ?></h2>
+ <form method="post" action="options.php"> <?php
+ settings_fields( 'better-related_options' ); ?>
+ <h3><?php _e( 'Main Options', 'better-related' ) ?></h3>
+ <table class="form-table form-table-clearnone" > <?php
+ $this->hidden(
+ 'mtime'
+ );
+ $this->hidden(
+ 'version'
+ );
+ // get relevant taxonomies
+ $taxonomies = get_taxonomies( array(
+ 'public' => true,
+ ) );
+ $remove = array_search( 'nav_menu', $taxonomies );
+ if ( $remove )
+ unset( $taxonomies[$remove] );
+
+ // get relevant post types
+ $post_types = get_post_types( array(
+ 'public' => true,
+ ) );
+ // @todo, hm, keep attachments? make them an option?
+ $remove = array_search( 'attachment', $post_types );
+ if ( $remove )
+ unset( $post_types[$remove] );
+
+ $this->checkbox(
+ sprintf( '<strong>%s</strong>', __( 'Automatically show related content on', 'better-related' ) ),
+ array( 'autoshowpt', $post_types ),
+ __( 'This option will add a link to the plugin\'s website by default, see below for options, or see the <tt>readme.txt</tt> for manual placement. ', 'better-related' )
+ );
+ $this->checkbox(
+ __( 'Automatically in feed', 'better-related' ),
+ 'autoshowrss'
+ ); ?>
+ </table>
+
+ <h3><?php _e( 'Display Options', 'better-related' ) ?></h3>
+ <table class="form-table form-table-clearnone" > <?php
+ $this->input(
+ __( 'Max results', 'better-related' ),
+ 'maxresults',
+ __( 'How many related posts to show.', 'better-related' )
+ );
+ $this->input(
+ __( 'Content filter priority', 'better-related' ),
+ 'filterpriority',
+ __( 'Changing this value might change the position of the related content list on your pages.', 'better-related' )
+ );
+ $this->input(
+ __( 'Related posts title', 'better-related' ),
+ 'relatedtitle',
+ false,
+ 60
+ );
+ $this->input(
+ __( 'No related posts found text', 'better-related' ),
+ 'relatednone',
+ __( 'Leave empty for no output at all.', 'better-related' ),
+ 60
+ );
+ $this->checkbox(
+ __( 'Display scoring details', 'better-related' ),
+ 'showdetails',
+ __( 'If this is enabled logged in admins will see the exact score for each related post on the frontend. There will also be some information about performed database queries and query time.', 'better-related' )
+ );
+ $this->checkbox(
+ __( 'Add default stylesheet', 'better-related' ),
+ 'stylesheet'
+ );
+ $this->select(
+ __( 'Promote the plugin', 'better-related' ),
+ 'thanks',
+ array(
+ //'title' => 'Link the title',
+ 'below' => __( 'Link below related posts', 'better-related' ),
+ 'info' => __( 'Tiny info-link next to the title', 'better-related' ),
+ 'none' => __( 'No, don\'t promote', 'better-related' )
+ ),
+ __( 'Please thank the plugin author by linking to the plugin\'s page or consider blogging about the plugin if you don\'t.', 'better-related' )
+ );
+ ?>
+ </table>
+
+ <p class="submit">
+ <input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
+ </p>
+
+ <?php /*
+ <h3><?php _e( 'Advanced Options', 'better-related' ) ?></h3>
+
+ <h4><?php _e( 'Scoring Presets', 'better-related' ) ?></h4>
+
+ <p><?php
+ _e( 'The presets are a very simply way of getting started. It is recommended to pick one of the small, medium and big options.', 'better-related' );
+ echo ' ';
+ _e( '<strong>Small</strong> is ideal for blogs with not too much traffic and less than 1000 posts.', 'better-related' ); ?>
+ </p>
+
+ <table class="form-table form-table-clearnone" > <?php
+ $this->select(
+ __( 'Scoring presets', 'better-related' ),
+ 'preset',
+ array(
+ 'simple' => 'Quick and dirty',
+ 'small' => 'Small site',
+ 'medium' => 'Medium site',
+ 'big' => 'Big site',
+ ),
+ __( 'TODO, description', 'better-related' )
+ ); ?>
+ </table>
+
+ <p class="submit">
+ <input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
+ </p>
+ */ ?>
+
+ <h4><?php _e( 'Scoring Options', 'better-related' ) ?></h4>
+
+ <p><?php
+ _e( 'You can choose a multiplier for each scoring method the plugin uses. Setting the multiplier to zero will disable the scoring method, and lead to less database queries.', 'better-related' ); ?>
+ </p>
+
+ <table class="form-table form-table-clearnone" > <?php
+ $this->input(
+ __( 'Minimum related score', 'better-related' ),
+ 'minscore',
+ __( 'Minimum related necessary to be listed.', 'better-related' )
+ );
+ $this->checkbox(
+ __( 'Find post types', 'better-related' ),
+ array( 'usept', $post_types ),
+ __( 'Post types to find as related.', 'better-related' )
+ );
+ $this->checkbox(
+ __( 'Use taxonomies', 'better-related' ),
+ array( 'usetax', $taxonomies )
+ );
+ $this->input(
+ __( 'Content to content multiplier', 'better-related' ),
+ 'do_c2c'
+ );
+ $this->input(
+ __( 'Title to title multiplier', 'better-related' ),
+ 'do_t2t'
+ );
+ $this->input(
+ __( 'Title to content multiplier', 'better-related' ),
+ 'do_t2c'
+ );
+ $this->input(
+ __( 'Keywords to content multiplier', 'better-related' ),
+ 'do_k2c'
+ );
+ $this->input(
+ __( 'Keywords to title multiplier', 'better-related' ),
+ 'do_k2t'
+ );
+ $this->input(
+ __( 'Term to taxonomy multiplier', 'better-related' ),
+ 'do_x2x'
+ ); ?>
+ </table>
+
+ <p class="submit">
+ <input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
+ </p>
+
+ <h4><?php _e( 'Performance Options', 'better-related' ) ?></h4>
+ <table class="form-table form-table-clearnone" > <?php
+ /*
+ $this->select(
+ __( 'Cache clearing behaviour', 'better-related' ),
+ 'clearcache',
+ array(
+ 'never' => __( 'Never clear', 'better-related' ),
+ 'publish' => __( 'Clear all on publish', 'better-related' ),
+ 'related' => __( 'Clear most related posts only', 'better-related' )
+ ),
+ __( 'If the relatedness scores should be deleted when posts are published. TODO', 'better-related' )
+ );
+ */
+ $this->input(
+ __( 'Query limit', 'better-related' ),
+ 'querylimit'
+ );
+ $this->checkbox(
+ __( 'Incremental scoring', 'better-related' ),
+ 'incremental'
+ );
+ $this->input(
+ __( 'Total query limit', 'better-related' ),
+ 't_querylimit'
+ ); ?>
+ </table>
+
+ <p class="submit">
+ <input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
+ </p>
+
+ <h4><?php _e( 'Developer Options', 'better-related' ) ?></h4>
+ <table class="form-table form-table-clearnone" > <?php
+ $this->select(
+ __( 'Data storage engine', 'better-related' ),
+ 'storage',
+ array(
+ 'postmeta' => __( 'post meta', 'better-related' ),
+ 'transient' => __( 'transient', 'better-related' )
+ ),
+ __( 'Which storage engine is used for caching. Transients should only be used to test various scoring configurations.', 'better-related' )
+ );
+ $this->input(
+ __( 'Data storage ID', 'better-related' ),
+ 'storage_id',
+ __( 'The prefix for the post meta or transient. I strongly recommend not to change this value while using post meta as storage.', 'better-related' )
+ );
+ $this->input(
+ __( 'Transient expiration time', 'better-related' ),
+ 'cachetime',
+ __( 'In seconds, only used if the relatedness scores are saved as transients.', 'better-related' )
+ );
+ $this->checkbox(
+ __( 'Enable logging', 'better-related' ),
+ 'log'
+ );
+ $this->select(
+ __( 'Which events to log', 'better-related' ),
+ 'loglevel',
+ array(
+ '' => '',
+ 'all' => __( 'all', 'better-related' ),
+ 'filter' => __( 'filter', 'better-related' ),
+ 'storage' => __( 'storage', 'better-related' ),
+ 'global' => __( 'global', 'better-related' ),
+ 'stopwords' => __( 'stopwords', 'better-related' ),
+ 'taxscore' => __( 'taxscore', 'better-related' ),
+ 'query' => __( 'db queries', 'better-related' ),
+ 'cscore' => __( 'cscore', 'better-related' )
+ )
+ ); ?>
+ </table>
+
+ <p class="submit">
+ <input type="submit" class="button-primary" value="<?php _e('Save Changes') ?>" />
+ </p>
+ </form>
+
+ </div> <?php
+ require_once( 'nkuttler.php' );
+ nkuttler0_2_3_links(
+ 'better-related',
+ 'http://www.nkuttler.de/wordpress-plugin/wordpress-related-posts-plugin/'
+ ); ?>
+ </div> <?php
+ }
+
+ /**
+ * Update content modification timestamp
+ *
+ * We need to update the timestamp every time some user-visible content
+ * is updated.
+ *
+ * @todo does this handle updates to published posts?
+ * @since 0.3.5
+ *
+ * @param int $id Post ID
+ * @return none
+ */
+ public function timestamp_content( $id ) {
+ $post = get_post( $id );
+ if ( $post->post_status == 'publish' || $post->post_status == 'trash' ) {
+ $this->log( "update mtime, post $id updated", 'storage' );
+ $this->update_option( 'mtime', time() );
+ }
+ }
+
+}
507 inc/frontend.php
@@ -0,0 +1,507 @@
+<?php
+
+/**
+ * @package better-related
+ * @subpackage frontend
+ * @since 0.1
+ */
+class BetterRelatedFrontend extends BetterRelated {
+
+ /**
+ * Constructor, set up the frontend
+ *
+ * @since 0.0.1
+ *
+ * @return none
+ */
+ public function __construct() {
+ BetterRelated::__construct();
+ add_filter(
+ 'the_content',
+ array( $this, 'auto_add' ),
+ $this->get_option( 'filterpriority' )
+ );
+ add_filter(
+ 'better_related_sort_score',
+ array( $this, 'filter_score' ),
+ 10
+ );
+ if ( $this->get_option( 'stylesheet' ) )
+ add_action( 'wp_print_styles', array( $this, 'css' ) );
+ #add_action( '{$prefix}footer', array( $this, 'debug' ) );
+ }
+
+ /**
+ * Include our default css
+ *
+ * @since 0.3.5
+ *
+ * @return none
+ */
+ public function css() {
+ wp_register_style(
+ 'better-related-frontend',
+ plugins_url( basename( $this->plugin_dir ) . '/css/better-related.css' ),
+ null,
+ '0.3.5'
+ );
+ wp_enqueue_style( 'better-related-frontend' );
+ }
+
+ /**
+ * Tempate tag helper, echos HTML markup of related posts
+ *
+ * @todo document: as we can override the config, consecutive calls of
+ * the_related() keep the new config
+ * @todo this works without parameter (?), why?
+ * @since 0.0.1
+ *
+ * @param integer $id post ID
+ * @param array $config config to use for this and the scorer object
+ */
+ public function the_related( $id, $config = null ) {
+ if ( isset( $config ) )
+ $this->override_options( $config );
+ echo $this->get_the_related( $id, $config );
+ }
+
+ /**
+ * Template tag helper, returns scores for a post
+ *
+ * @since 0.3.7
+ *
+ * @param int $id Post ID
+ * @param array $config config to use for this and the scorer object
+ * @return array Scores for a post
+ */
+ public function get_the_scores( $id, $config = null ) {
+ $scores = $this->get_score_for_post( $id, $config );
+ return $scores[$id];
+ }
+
+ /**
+ * Tempate tag helper, returns HTML markup of related posts
+ *
+ * @since 0.2.2
+ *
+ * @param integer $id post ID
+ * @param array $config config to use for this and the scorer object
+ * @return string HTML markup
+ */
+ private function get_the_related( $id, $config = null ) {
+ $scores = $this->get_score_for_post( $id, $config );
+ if ( isset( $scores[$id] ) ) {
+ $queries = $scores['queries'];
+ $qtime = $scores['etime'] - $scores['stime'];
+ $offset = $scores['offset'];
+ $scores = apply_filters( 'better_related_sort_score', $scores[$id] );
+ }
+ $related = '';
+ if ( $scores && count( $scores ) ) {
+ $post_types = $this->get_option( 'usept' );
+ if ( !is_array( $post_types ) )
+ $post_types = array( get_post_type() );
+ else
+ $post_types = array_keys( $post_types );
+ $listed = 0;
+ foreach( $scores as $id => $score ) {
+ if ( $score > $this->get_option( 'minscore' ) ) {
+ if ( $listed >= $this->get_option( 'maxresults' ) )
+ break;
+ $listed++;
+ $link = get_permalink( $id );
+ $title = get_the_title( $id );
+ $post_type = get_post_type( $id );
+ if ( !in_array( $post_type, $post_types ) )
+ continue;
+ if ( !$title ) {
+ $title = __(
+ 'This entry has no title',
+ 'better-related'
+ );
+ }
+ $description = $title;
+ $showscore = '';
+ if ( current_user_can( 'manage_options' ) && $this->get_option( 'showdetails' ) ) {
+ $showscore = sprintf(
+ "<span class=\"score\">(%.3f)</span>",
+ $score
+ );
+ }
+ $related .= "<li> <a href=\"$link\" title=\"Permanent link to $description\">$title</a> $showscore </li>\n";
+ }
+ }
+ }
+ if ( $related ) {
+ // thanks link
+ $atitle = __( 'Related content found by the Better Related Posts plugin', 'better-related' );
+ $ahref = 'http://www.nkuttler.de/wordpress-plugin/wordpress-related-posts-plugin/';
+ // @todo markup should probably be configurable somehow
+ $pre = '<div class="betterrelated">';
+ if ( $relatedtitle = $this->get_option( 'relatedtitle' ) ) {
+ $pre .= '<p>';
+ $pre .= $relatedtitle;
+ if ( $this->get_option( 'thanks' ) == 'info' ) {
+ $pre .= '<sup><a class="thanks" style="text-decoration: none;" href="' . $ahref . '" title="' . $atitle . '">?</a></sup>';
+ }
+ if ( current_user_can( 'manage_options' ) && $this->get_option( 'showdetails' ) ) {
+ $pre .= sprintf(
+ __( "<span class=\"score\">%s queries in %1.4f seconds, current offset %s</span>", 'better-related' ),
+ $queries,
+ $qtime,
+ $offset
+ );
+ }
+ $pre .= "</p>\n";
+ }
+ $r = $pre . '<ol>' . $related . '</ol>';
+ if ( $this->get_option( 'thanks' ) == 'below' ) {
+ $anchor = __( 'Better Related Posts Plugin', 'better-related' );
+ $r .= '<a class="thanks" style="font-size: smaller; text-decoration: none;" title="' . $atitle . '" href="' . $ahref . '">' . $anchor . '</a>';
+ }
+ $r .= '</div>';
+ }
+ elseif ( $relatednone = $this->get_option( 'relatednone' ) ) {
+ $r = '<div class="betterrelated none">';
+ $r .= $relatednone;
+ $r .= '</div>';
+ }
+ return $r;
+ }
+
+ /**
+ * Return a score set
+ *
+ * @since 0.3.6
+ *
+ * @param int $id Post ID
+ * @param array $config different config to use for this and the scorer object
+ * @return array Scores for a post
+ */
+ private function get_score_for_post( $id, $config = null ) {
+ require_once( 'scorer.php' );
+ $BetterRelatedScorer = new BetterRelatedScorer( $config );
+ $scores = $BetterRelatedScorer->get_score_for_post( $id );
+ if ( isset( $scores[$id] ) ) {
+ $scores[$id] = apply_filters(
+ 'better_related_sort_score',
+ $scores[$id]
+ );
+ return $scores;
+ }
+ return false;
+ }
+
+ /**
+ * Return a score set for a string
+ *
+ * @since 0.4.2
+ *
+ * @param string $string search string
+ * @param array $config different config to use for this and the scorer object
+ * @return array Scores for a post
+ */
+ public function get_score_for_string( $string, $config = null ) {
+ require_once( 'scorer.php' );
+ $BetterRelatedScorer = new BetterRelatedScorer( $config );
+ $scores = $BetterRelatedScorer->get_score_for_string( $string );
+ if ( isset( $scores[0] ) ) {
+ $scores[0] = apply_filters(
+ 'better_related_sort_score',
+ $scores[0]
+ );
+ return $scores[0];
+ }
+ return false;
+ }
+
+ /**
+ * Filter that automatically adds a related content list to post contents.
+ *
+ * @since 0.2.3
+ * @todo insert at the top (?!)
+ *
+ * @param string $content content
+ * @return string $content content
+ */
+ public function auto_add( $content ) {
+ $post_types = $this->get_option( 'autoshowpt' );
+ if ( is_feed() && $this->get_option( 'autoshowrss' ) )
+ $content = $content . $this->get_the_related( get_the_ID() );
+ elseif ( !is_single() )
+ return $content;
+ elseif ( is_array( $post_types ) )
+ foreach ( $post_types as $post_type => $value )
+ if ( get_post_type() == $post_type && $value )
+ $content = $content . $this->get_the_related( get_the_ID() );
+ return $content;
+ }
+
+ /**
+ * Filter the scores before we display them. Used by the plugin for sorting
+ *
+ * The scores are always ordered by relevance, however it is possible to
+ * ignore the order through custom loops.
+ *
+ * @todo convert manual scores to floats
+ * @since 0.0.2
+ *
+ * @param array $score A score array
+ * @return array The sorted scores
+ */
+ public function filter_score( $score ) {
+ if ( !is_array( $score) )
+ return false;
+ //$score = array_filter( $score, array( $this, 'minscore' ) );
+ arsort( $score );
+ //$maxresults = $this->get_option( 'maxresults' );
+ //$score = array_slice( $score, 0, $maxresults, true );
+ return $score;
+ }
+
+ /**
+ * Array filter helper, only let minimum score through.
+ *
+ * @since 0.0.2
+ *
+ * @param integer $score Relatedness score
+ * @return boolean Minimum score reached
+ */
+ private function minscore( $score ) {
+ $minscore = $this->get_option( 'minscore' );;
+ if ( $score > $minscore )
+ return true;
+ return false;
+ }
+
+ /**
+ * Debug queries, set define('SAVEQUERIES', true);
+ *
+ * @since 0.0.1
+ *
+ * @return none
+ */
+ private function debug() {
+ global $wpdb;
+ foreach( $wpdb->queries as $query )
+ echo '<input size=150 value="' . $query[0] . '"><br>';
+ }
+
+}
+
+/**
+ * Template tag
+ *
+ * @since unknown
+ *
+ * @param interger $id post id
+ * @param array $config different config to pass to the scorer object
+ * @return none
+ */
+if ( !function_exists( 'the_related' ) ) {
+ function the_related( $id = null, $config = null ) {
+ if ( !isset( $id ) || !is_integer( $id ) )
+ $id = get_the_ID();
+ global $BetterRelatedFrontend;
+ $BetterRelatedFrontend->the_related( $id, $config );
+ }
+}
+
+/**
+ * Template tag that returns the scores
+ *
+ * @since 0.3.6
+ *
+ * @param interger $id post id
+ * @param array $config different config to pass to the scorer object
+ * @return array A score set
+ */
+if ( !function_exists( 'the_related_get_scores' ) ) {
+ function the_related_get_scores( $id = null, $config = null ) {
+ if ( !isset( $id ) || !is_integer( $id ) )
+ $id = get_the_ID();
+ global $BetterRelatedFrontend;
+ return $BetterRelatedFrontend->get_the_scores( $id, $config );
+ }
+}
+
+/**
+ * Template tag to analyze the various scoring methods
+ *
+ * @since 0.3.1
+ *
+ * @param $minscore Minimum score to show
+ * @param $maxresults Maximum results to show
+ * @return none
+ */
+if ( !function_exists( 'the_related_analyze' ) ) {
+ function the_related_analyze( $minscore = 0, $maxresults = 10 ) {
+ echo '<hr><strong>content to content</strong>';
+ the_related(
+ get_the_ID(),
+ array(
+ 'do_t2t' => 0,
+ 'do_t2c' => 0,
+ 'do_c2c' => 1,
+ 'do_k2c' => 0,
+ 'do_k2t' => 0,
+ 'do_x2x' => 0,
+ 'storage' => 'transient',
+ 'storage_id' => 'd1-',
+ 'cachetime' => 1,
+ 'minscore' => $minscore,
+ 'maxresults' => $maxresults,
+ 'thanks' => false,
+ 'showdetails' => true,
+ )
+ );
+ echo '<hr><strong>title to content</strong>';
+ the_related(
+ get_the_ID(),
+ array(
+ 'do_t2t' => 0,
+ 'do_t2c' => 1,
+ 'do_c2c' => 0,
+ 'do_k2c' => 0,
+ 'do_k2t' => 0,
+ 'do_x2x' => 0,
+ 'storage' => 'transient',
+ 'storage_id' => 'd2-',
+ 'cachetime' => 1,
+ 'minscore' => $minscore,
+ 'maxresults' => $maxresults,
+ 'thanks' => false,
+ 'showdetails' => true
+ )
+ );
+ echo '<hr><strong>keywords to content</strong>';
+ the_related(
+ get_the_ID(),
+ array(
+ 'do_t2t' => 0,
+ 'do_t2c' => 0,
+ 'do_c2c' => 0,
+ 'do_k2c' => 1,
+ 'do_k2t' => 0,
+ 'do_x2x' => 0,
+ 'storage' => 'transient',
+ 'storage_id' => 'd3-',
+ 'cachetime' => 1,
+ 'minscore' => $minscore,
+ 'maxresults' => $maxresults,
+ 'thanks' => false,
+ 'showdetails' => true
+ )
+ );
+ echo '<hr><strong>title to title</strong>';
+ the_related(
+ get_the_ID(),
+ array(
+ 'do_t2t' => 1,
+ 'do_t2c' => 0,
+ 'do_c2c' => 0,
+ 'do_k2c' => 0,
+ 'do_k2t' => 0,
+ 'do_x2x' => 0,
+ 'storage' => 'transient',
+ 'storage_id' => 'd6-',
+ 'cachetime' => 1,
+ 'minscore' => $minscore,
+ 'maxresults' => $maxresults,
+ 'thanks' => false,
+ 'showdetails' => true
+ )
+ );
+ echo '<hr><strong>keywords to title</strong>';
+ the_related(
+ get_the_ID(),
+ array(
+ 'do_t2t' => 0,
+ 'do_t2c' => 0,
+ 'do_c2c' => 0,
+ 'do_k2c' => 0,
+ 'do_k2t' => 1,
+ 'do_x2x' => 0,
+ 'storage' => 'transient',
+ 'storage_id' => 'd5-',
+ 'cachetime' => 1,
+ 'minscore' => $minscore,
+ 'maxresults' => $maxresults,
+ 'thanks' => false,
+ 'showdetails' => true
+ )
+ );
+ echo '<hr><strong>terms to taxonomies</strong>';
+ the_related(
+ get_the_ID(),
+ array(
+ 'do_t2t' => 0,
+ 'do_t2c' => 0,
+ 'do_c2c' => 0,
+ 'do_k2c' => 0,
+ 'do_k2t' => 0,
+ 'do_x2x' => 1,
+ 'storage' => 'transient',
+ 'storage_id' => 'd4-',
+ 'cachetime' => 1,
+ 'minscore' => $minscore,
+ 'maxresults' => $maxresults,
+ 'thanks' => false,
+ 'showdetails' => true
+ )
+ );
+ }
+}
+
+/**
+ * Template tag, find posts related to a string
+ *
+ * @since 0.4.2
+ *
+ * @param string $string search string
+ * @return array A score set
+ */
+if ( !function_exists( 'get_the_related_for_string' ) ) {
+ function get_the_related_for_string( $string, $config = null ) {
+ global $BetterRelatedFrontend;
+ return $BetterRelatedFrontend->get_score_for_string( $string, $config );
+ }
+}
+
+/**
+ * Template tag, list posts related to a string
+ *
+ * @since 0.4.2
+ * @todo should the output be moved into the class to be consistent?
+ *
+ * @param string $string search string
+ * @return array A score set
+ */
+if ( !function_exists( 'the_related_for_string' ) ) {
+ function the_related_for_string( $string, $maxresults = 5, $config = null ) {
+ $scores = get_the_related_for_string( $string, $config );
+ $listed = 0;
+ foreach( $scores as $id => $score ) {
+ if ( $listed >= $maxresults )
+ break;
+ if ( $score == 0 )
+ break;
+ $listed++;
+ $link = get_permalink( $id );
+ $title = get_the_title( $id );
+ $post_type = get_post_type( $id );
+ if ( !$title ) {
+ $title = __(
+ 'This entry has no title',
+ 'better-related'
+ );
+ }
+ $description = $title;
+ $related .= "<li> <a href=\"$link\" title=\"Permanent link to $description\">$title</a></li>\n";
+ }
+ if ( @$related )
+ echo '<ul>' . $related . '</ul>';
+ elseif ( $relatednone = $config['relatednone'] )
+ echo $relatednone;
+ }
+}
63 inc/nkuttler.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * Information about the author 0.2.3
+ */
+
+if ( !function_exists( 'nkuttler0_2_3_links' ) ) {
+ function nkuttler0_2_3_links( $plugin, $url_plugin = false ) {
+
+ $name = 'Nicolas Kuttler';
+ $gravatar = '7b75fc655756dd5c58f4df1f4083d2e2.jpg';
+ $url_author = 'http://www.nkuttler.de/';
+ if ( !$url_plugin )
+ $url_plugin = $url_author . "wordpress/$plugin/";
+ $feedburner = 'http://feedburner.google.com/fb/a/mailverify?uri=NicolasKuttler&loc=en_US'; // subscribe feed per mail
+ $profile = 'http://wordpress.org/extend/plugins/profile/nkuttler/';
+
+ /***/
+
+ $vote = "http://wordpress.org/extend/plugins/$plugin/";
+ $homeFeed = $url_author . 'feed/';
+ $donate = $url_author . 'wordpress/donations/';
+ $commentsFeed = $url_plugin . 'feed/'; ?>
+
+ <div id="nkbox" >
+ <strong><?php _e( 'Do you like this plugin?', $plugin ) ?></strong>
+ <div class="gravatar" >
+ <a href="<?php echo $url_author ?>"><img src="http://www.gravatar.com/avatar/<?php echo $gravatar ?>?s=50" alt="<?php echo $name ?>" title="<?php echo $name ?>" /></a>
+ <br />
+ <?php echo $name ?>
+ </div>
+ <ul >
+ <li>
+ <?php printf( __( "<a href=\"%s\">Rate</a> it", $plugin ), $vote ) ?> <br />
+ </li>
+ <li>
+ <?php printf( __( "<a href=\"%s\">Visit</a> it's homepage", $plugin ), $url_plugin ) ?> <br />
+ </li>
+ <li>
+ <?php printf( __( "<a href=\"%s\">Subscribe</a> the feed", $plugin ), $commentsFeed ) ?> <br />
+ </li>
+ <li>
+ <?php printf( __( "<a href=\"%s\">Donate</a>!", $plugin ), $donate ) ?> <br />
+ </li>
+ </ul>
+ <strong><?php _e( 'About the author', $plugin ) ?></strong> <br />
+ <ul >
+ <li>
+ <?php printf( __( 'My <a href="%s">blog</a>', $plugin ), $url_author ) ?> <br />
+ </li>
+ <li>
+ <?php printf( __( "Subscribe via <a href=\"%s\">RSS</a> or <a href=\"%s\">email</a>", $plugin ), $homeFeed, $feedburner ) ?> <br />
+ </li>
+ </ul>
+ <div >
+ <a href="<?php echo $profile ?>"><?php _e( 'My other plugins', $plugin ) ?></a><br />
+ <?php _e( '<a href="http://www.nkuttler.de">Translated by Nicolas</a>', $plugin ) ?><br />
+ </div>
+ </div> <?php
+ }
+}
+
+?>
506 inc/scorer.php
@@ -0,0 +1,506 @@
+<?php
+
+/**
+ * @package better-related
+ * @subpackage scorer
+ * @since 0.1
+ */
+class BetterRelatedScorer extends BetterRelated {
+
+ /**
+ * Temporarily hold the related score of one or many posts
+ *
+ * @since 0.0.1
+ *
+ * @var array postid => array of related posts => score of single post
+ */
+ private $score = array();
+
+ /**
+ * Constructor, set up the score variable
+ *
+ * @since 0.0.1
+ *
+ * @return none
+ */
+ public function __construct( $config = null ) {
+ BetterRelated::__construct();
+ if ( isset( $config ) )
+ $this->override_options( $config );
+ }
+
+ /**
+ * Return the score of related posts
+ *
+ * Generate the score if necessary and store it using one of the storage
+ * backends.
+ *
+ * @todo check post meta for manual relatedness scores
+ * @since 0.0.1
+ *
+ * @param int $id Post ID
+ * @return array Posts IDs and their relatedness score
+ */
+ public function get_score_for_post( $id = null ) {
+ switch ( $this->get_option( 'storage' ) ) {
+ case 'transient':
+ $score = get_transient( $this->get_option( 'storage_id' ) . $id );
+ break;
+ default:
+ case 'postmeta':
+ $score = get_post_meta(
+ $id,
+ $this->get_option( 'storage_id' ),
+ true
+ );
+ break;
+ }
+ $offset = 0;
+ if ( $score ) {
+ $offset = $this->get_offset( $score );
+ if ( $score['ctime'] >= $this->get_option( 'mtime' ) && !$offset ) {
+ return $score;
+ }
+ elseif( !$offset ) {
+ $this->log( "stale score for post $id", 'storage' );
+ }
+ else{
+ $this->log( "preserve old score, offset $offset", 'storage' );
+ $this->score = $score;
+ }
+ }
+ $this->build_score_for_post( $id, $offset );
+ $this->save_score_for_post( $id );
+ return $this->score;
+ }
+
+ /**
+ * Find related posts for a string.
+ *
+ * This is intended to be used instead of the built-in WordPress search.
+ *
+ * @since 0.4.2
+ *
+ * @return array A score array
+ */
+ public function get_score_for_string( $string, $do_s2t = 1, $do_s2c = 1 ) {
+ if ( $weight = $do_s2t ) {
+ $this->score_by_content(
+ 0,
+ $string,
+ 'post_title',
+ $weight,
+ 0
+ );
+ }
+ if ( $weight = $do_s2c ) {
+ $this->score_by_content(
+ 0,
+ $string,
+ 'post_content',
+ $weight,
+ 0
+ );
+ }
+ return $this->score;
+ }
+
+ /**
+ * Calculate the offset for the new query
+ *
+ * @since 0.3.5
+ *
+ * @param array $score a score set
+ * @return int offset
+ */
+ private function get_offset( $score ) {
+ if ( $this->get_option( 'incremental' ) ) {
+ $offset = $score['offset'];
+ if ( $offset < $this->get_option( 't_querylimit' ) ) {
+ $offset += $this->get_option( 'querylimit' );
+ return $offset;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Save the score for a post
+ *
+ * @since 0.2.7
+ *
+ * @param integer $id post id
+ * @return set_transient success
+ */
+ private function save_score_for_post( $id ) {
+ $this->timestamp_score();
+ $storage = $this->get_option( 'storage' );
+ switch ( $storage ) {
+ case 'transient':
+ $success = set_transient(
+ $this->get_option( 'storage_id' ) . $id,
+ $this->score,
+ $this->get_option( 'cachetime' )
+ );
+ break;
+ default:
+ case 'postmeta':
+ $success = update_post_meta(
+ $id,
+ $this->get_option( 'storage_id' ),
+ $this->score
+ );
+ break;
+ }
+ $this->log( "saved score for $id ($storage): $success", 'storage' );
+ }
+
+ /**
+ * Build the score list for a single post
+ *
+ * @since 0.0.1
+ *
+ * @param integer $id post id
+ * @return none
+ */
+ private function build_score_for_post( $id, $offset = 0 ) {
+ if ( !$id ) // check why this gets called twice, and $id would be empty
+ return;
+ $this->score['offset'] = $offset;
+ $this->score['stime'] = $this->microtime( true );
+ $this->score['queries'] = 0;
+ $this->build_score_for_content( $id, $offset );
+ if ( $weight = $this->get_option( 'do_x2x' ) )
+ $this->build_score_for_taxonomies( $id, $weight, $offset );
+ $this->score['etime'] = $this->microtime( true );
+ }
+
+ /**
+ * Build the score for a post's content
+ *
+ * This uses various methods which can all be enabled/disabled.
+ *
+ * @since 0.2
+ *
+ * @param integer $id Post ID
+ * @return none
+ */
+ private function build_score_for_content( $id, $offset ) {
+ // @todo only if k2c || k2t
+ $content = get_the_content( $id );
+ $title = get_the_title( $id );
+ $keywords = $this->get_keywords( $id );
+ if ( $weight = $this->get_option( 'do_t2t' ) )
+ $this->score_by_content(
+ $id,
+ $title,
+ 'post_title',
+ $weight,
+ $offset
+ );
+ if ( $weight = $this->get_option( 'do_t2c' ) )
+ $this->score_by_content(
+ $id,
+ $title,
+ 'post_content',
+ $weight,
+ $offset
+ );
+ if ( $weight = $this->get_option( 'do_c2c' ) )
+ $this->score_by_content(
+ $id,
+ $content,
+ 'post_content',
+ $weight,
+ $offset
+ );
+ if ( is_array( $keywords ) ) {
+ $keywords = implode( ' ', $keywords );
+ if ( $weight = $this->get_option( 'do_k2c' ) )
+ $this->score_by_content(
+ $id,
+ $keywords,
+ 'post_content',
+ $weight,
+ $offset
+ );
+ if ( $weight = $this->get_option( 'do_k2t' ) )
+ $this->score_by_content(
+ $id,
+ $keywords,
+ 'post_title',
+ $weight,
+ $offset
+ );
+ }
+ }
+
+ /**
+ * Return all terms of a post. Use all public taxonomies,
+ *
+ * @since 0.2.7
+ *
+ * @param integer $id post ID
+ * @return array of terms
+ */
+ private function get_keywords( $id ) {
+ // Get category terms
+ if ( $cats = get_the_category( $id ) )
+ foreach( $cats as $c )
+ $keywords[] = $c->cat_name;
+ // Get tags terms
+ if ( $tags = get_the_tags( $id ) )
+ foreach ( $tags as $tag )
+ $keywords[] = $tag->name;
+ // Get terms of all custom taxonomies
+ $taxonomies = get_taxonomies( array(
+ 'public' => true,
+ '_builtin' => false
+ ) );
+ if ( $taxonomies )
+ foreach ( $taxonomies as $taxonomy )
+ if ( $terms = get_the_terms( $id, $taxonomy ) )
+ foreach ( $terms as $term )
+ $keywords[] = $term->name;
+ return $keywords;
+ }
+
+ /**
+ * Build a list of related posts by content and award a score
+ *
+ * This uses the mysql relatedness score for fulltexts, see
+ * http://dev.mysql.com/doc/refman/5.0/en/fulltext-search.html . We score
+ * against post_content at the moment, which can include HTML markup. This
+ * could probably be improved but would increase disk space consumption
+ * because either the_content_filtered or a new column/table would be
+ * necessary.
+ *
+ * @since 0.2
+ * @todo limit by date
+ * @todo multisite -> related post on a network
+ * @todo search topic + content at once (?)
+ *
+ * @param int $id Post ID
+ * @param string $string String to search for in other post's content
+ * @param string $column DB column to search
+ * @param float $weight Scoring weight
+ * @param int offset Query offset
+ * @return none
+ */
+ private function score_by_content( $id, $string, $column = 'post_content', $weight, $offset ) {
+ global $wpdb;
+ $prefix = $wpdb->prefix;
+ $string = $wpdb->escape( $string );
+ // don't care about post types @todo update the docs, say that different
+ // post types don't necessarily require different storage
+ $query = "
+ SELECT ID,
+ MATCH ($column) AGAINST ('$string')
+ FROM {$prefix}posts
+ WHERE ( post_status='publish' OR post_status = 'private' )
+ AND ID != $id ";
+ // @todo Wouldn't it be better to find all post types and do the
+ // filtering in the display?
+ if ( $this->get_option( 'usept' ) )
+ $post_type = array_keys( $this->get_option( 'usept' ) );
+ if ( is_string( $post_type ) ) {
+ $query .= " AND {$prefix}posts.post_type = '$post_type' ";
+ }
+ elseif( is_array( $post_type ) ) {
+ $query .= " AND ( ";
+ $multiple = false;
+ foreach( $post_type as $type ) {
+ if ( $multiple )
+ $query .= ' OR ';
+ $query .= " {$prefix}posts.post_type = '$type' ";
+ $multiple = true;
+ }
+ $query .= " )\n";
+ }
+ $query .= "
+ ORDER BY {$prefix}posts.post_date DESC ";
+ if ( $limit = $this->get_option( 'querylimit' ) )
+ $query .= " LIMIT $limit ";
+ if ( $offset )
+ $query .= " OFFSET $offset ";
+ $query .= ';';
+ $query = apply_filters( 'better-related-cquery', $query );
+ $this->log( $query, 'query' );
+ $posts = $wpdb->get_results( $wpdb->prepare( $query ), ARRAY_N );
+ $this->score['queries']++;
+ $this->log( print_r( $posts, true ), 'query' );
+ // $post[0] is the post ID
+ // $post[1] is the mysql relevance score
+ if ( $posts ) {
+ foreach( $posts as $post ) {
+ if ( !isset( $this->score[$id][$post[0]] ) )
+ $this->score[$id][$post[0]] = 0;
+ $this->score[$id][$post[0]] += $post[1] * $weight;
+ }
+ }
+ }
+
+ /**
+ * Award relatedness points for terms of a single post.
+ *
+ * Get a list of taxonomies the post supports, and get relatedness scores
+ * for every term the post uses.
+ *
+ * @since 0.0.1
+ *
+ * @param int $id Post ID of the post we want to find related posts for
+ * @param int $weight The multiplier to use for relatedness
+ * @return none
+ */
+ private function build_score_for_taxonomies( $id, $weight, $offset ) {
+ // @fixme this is reduntant!?
+ if ( !$this->get_option( 'do_x2x' ) )
+ return;
+ $post_type = get_post_type( $id );
+ $supported_taxes = get_post_taxonomies( $id );
+ $matches = array();
+ foreach( $supported_taxes as $tax ) {
+ $matches = array();
+ $post_terms = get_the_terms( $id, $tax );
+ if ( !is_array( $post_terms ) )
+ return;
+ $post_terms_count = count( $post_terms );
+ $taxonomy_terms = get_terms( $tax );
+ $tax_terms_count = count( $taxonomy_terms );
+ // find all matching terms and count them
+ $related_posts_count= 0;
+ foreach( $post_terms as $term ) {
+ $related_posts = $this->get_related_posts_by_term(
+ $id,
+ $tax,
+ $term->name,
+ $post_type,
+ $offset
+ );
+ $related_posts_count += count( $related_posts );
+ $matches[$term->name] = $related_posts;
+ }
+ // Determine importance of each match in the set.
+ // @todo option
+ // @todo different factor/weight for different taxonomies
+ // this is linear, which leads to matches in small taxonomies being
+ // very important. good or bad?
+ $factor = 100 / $tax_terms_count * $post_terms_count;
+ // At last, apply the scores
+ foreach ( $matches as $key => $matches ) {
+ foreach ( $matches as $match_id ) {
+ if ( !isset( $this->score[$id] ) )
+ $this->score[$id] = array();
+ if ( !isset( $this->score[$id][$match_id] ) )
+ $this->score[$id][$match_id] = 0;
+ $this->score[$id][$match_id] += $factor * $weight;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get a list of related posts by taxonomy, term and/or post type
+ *
+ * @todo limit by date
+ * @todo multisite, find related content on other blogs
+ * @since 0.0.1
+ *
+ * @param int $id Post ID
+ * @param str $taxonomy the taxonomy
+ * @param str $term the term
+ * @param mixed $post_type the post type as string or a post types array
+ * @return mixed array of post IDs
+ */
+ private function get_related_posts_by_term( $id, $taxonomy, $term, $post_type = false, $offset ) {
+ global $wpdb;
+ $prefix = $wpdb->prefix;
+ $term = $wpdb->escape( $term );
+ $query = "
+ SELECT SQL_CALC_FOUND_ROWS post_name, ID FROM {$prefix}posts
+ INNER JOIN {$prefix}term_relationships ON ({$prefix}posts.ID = {$prefix}term_relationships.object_id)
+ INNER JOIN {$prefix}term_taxonomy ON ({$prefix}term_relationships.term_taxonomy_id = {$prefix}term_taxonomy.term_taxonomy_id)
+ INNER JOIN {$prefix}terms ON ({$prefix}term_taxonomy.term_id = {$prefix}terms.term_id)
+ WHERE ID != $id
+ ";
+ // We can not apply different weights to different taxonomies. This
+ // doesn't look like an important feature at the moment.
+ if ( $this->get_option( 'usetax' ) )
+ $taxonomy = array_keys( $this->get_option( 'usetax' ) );
+ if ( is_string( $taxonomy ) )
+ $query .= " AND {$prefix}term_taxonomy.taxonomy = '$taxonomy' ";
+ elseif( is_array( $taxonomy ) ) {
+ $query .= " AND ( ";
+ $multiple = false;
+ foreach( $taxonomy as $tax ) {
+ if ( $multiple )
+ $query .= ' OR ';
+ $query .= " {$prefix}term_taxonomy.taxonomy = '$tax' ";
+ $multiple = true;
+ }
+ $query .= " )\n";
+ }
+ $query .= " AND {$prefix}terms.name IN ('$term')\n";
+ // @todo Wouldn't it be better to find all post types and do the
+ // filtering in the display?
+ if ( $this->get_option( 'usept' ) )
+ $post_type = array_keys( $this->get_option( 'usept' ) );
+ if ( is_string( $post_type ) ) {
+ $query .= " AND {$prefix}posts.post_type = '$post_type' ";
+ }
+ elseif( is_array( $post_type ) ) {
+ $query .= " AND ( ";
+ $multiple = false;
+ foreach( $post_type as $type ) {
+ if ( $multiple )
+ $query .= ' OR ';
+ $query .= " {$prefix}posts.post_type = '$type' ";
+ $multiple = true;
+ }
+ $query .= " )\n";
+ }
+ $query .= "
+ AND ({$prefix}posts.post_status = 'publish' OR {$prefix}posts.post_status = 'private')
+ GROUP BY {$prefix}posts.ID
+ ORDER BY {$prefix}posts.post_date DESC
+ ";
+ if ( $limit = $this->get_option( 'querylimit' ) )
+ $query .= " LIMIT $limit ";
+ if ( $offset )
+ $query .= " OFFSET $offset ";
+ $query .= ';';
+ $query = apply_filters( 'better-related-taxquery', $query );
+ $this->log( $query, 'query' );
+ $posts = $wpdb->get_results( $wpdb->prepare( $query ), OBJECT );
+ $this->score['queries']++;
+ $r = array();
+ foreach( @$posts as $post ) {
+ array_push( $r, $post->ID );
+ }
+ return $r;
+ }
+
+ /**
+ * Update score creation timestamp
+ *
+ * @since 0.3.5
+ *
+ * @return none
+ */
+ private function timestamp_score() {
+ $this->score['ctime'] = time();
+ }
+
+ /**
+ * Return timestamp as float with microseconds
+ *
+ * @since 0.3.5
+ *
+ * @return float timestamp
+ */
+ private function microtime() {
+ $string = explode( ' ', microtime() );
+ return intval( $string[1] ) + floatval( $string[0] );
+ }
+
+}
269 readme.txt
@@ -0,0 +1,269 @@
+=== Better Related Posts ===
+Contributors: nkuttler
+Author URI: http://www.nkuttler.de/
+Plugin URI: http://www.nkuttler.de/wordpress-plugin/better-related-posts-and-custom-post-types/
+Donate link: http://www.nkuttler.de/wordpress/donations/
+Tags: admin, plugin, related post, related custom post types, related custom taxonomies, i18n, l10n, internationalized, localized, cache, caching, transients, php5, mysql5
+Requires at least: 3.0
+Tested up to: 3.0
+Stable tag: 0.4.3.3
+
+Do you use custom post types and taxonomies? Do you want to list any related content, not just posts?
+
+== Description ==
+
+Custom post types are one the best features in WordPress. Since WordPress 3.0 they are much easier to use. Almost every theme I build for a client features at least one custom post type and usually a custom taxonomy as well.
+
+But there is a problem. There is no plugin that lists related posts that are from a custom post type. After looking through the sourcecode of a few plugins I decided to implement my own related content plugin.
+
+= Plugin Features =
+
+ * Depends on PHP5 and MySQL5
+ * Option to add related posts to the RSS feed
+ * Use fulltext indexes for good performance
+ * Does caching through post meta or transients
+ * Incremental scoring for sites with many posts
+ * Find related posts, pages and custom post types
+ * Score relationships by various MySQL relevance scores or term relationships
+ * Use tags, categories or custom taxonomies
+ * Internationalized, OO, hopefully well documented and readable
+
+= Other plugins I wrote =
+
+ * [Better Lorem Ipsum Generator](http://www.nkuttler.de/wordpress-plugin/wordpress-lorem-ipsum-generator-plugin/)
+ * [Better Related Posts](http://www.nkuttler.de/wordpress-plugin/wordpress-related-posts-plugin/)
+ * [Custom Avatars For Comments](http://www.nkuttler.de/wordpress-plugin/custom-avatars-for-comments/)
+ * [Better Tag Cloud](http://www.nkuttler.de/wordpress-plugin/a-better-tag-cloud-widget/)
+ * [Theme Switch](http://www.nkuttler.de/wordpress-plugin/theme-switch-and-preview-plugin/)
+ * [MU fast backend switch](http://www.nkuttler.de/wordpress-plugin/wpmu-switch-backend/)
+ * [Visitor Movies for WordPress](http://www.nkuttler.de/wordpress-plugin/record-movies-of-visitors/)
+ * [Zero Conf Mail](http://www.nkuttler.de/wordpress-plugin/zero-conf-mail/)
+ * [Move