<?php
/*
Plugin Name: XSPF playlist
Description: Makes a playlist from Wordpress attachments. Add <tt>?play=xspf</tt> to any post, Page, or archive URI.
Author: Sam
Version: 0.2

=== XSPF playlist ===
Tags: playlist, xspf, m3u, attachments
Requires at least: 2.5?
Tested up to: 2.7

Here is a short description of the plugin.  This should be no more than 150 chars. No markup here.

== Description ==

This is the long description.  No limit, and you can use Markdown…

== Installation ==

This section describes how to install the plugin and get it working.

*/

define( 'PM_CONTENT', false ); // save playlist files?

define( 'PM_CONTENT_DIRECTORY_NAME', 'play' ); // Name of directory in wp-content; stores playlists
define( 'PM_CONTENT_PATH', WP_CONTENT_DIR . DIRECTORY_SEPARATOR . PM_CONTENT_DIRECTORY_NAME );
define( 'PM_CONTENT_URL', WP_CONTENT_URL . '/' . PM_CONTENT_DIRECTORY_NAME . '/' );
define( 'PM_CONTENT_ALL_NAME', 'all' ); // Name of the playlist containing all playable attachments

define( 'PM_LOG', true );
define( 'PM_DEBUG', false );

define( 'PM_DEFAULT_PLAYLIST_FORMAT', 'xspf' ); // not used yet
define( 'PM_PLAYLIST_MAX_AGE', 600 ); // seconds for browsers/clients to keep cached copy

define( 'PM_BLOG_CHARSET', get_option('blog_charset') );

define( 'PM_REWRITE', true ); // do pretty permalinks?
	define(	'PM_REWRITE_ENDPOINT_NAME', 'play' );
	define(	'PM_REWRITE_ENDPOINT_PLACES'
		, EP_PERMALINK	// single post or page
		| EP_ROOT	// home or front
		| EP_ALL
	);

$Playlist_Maker = new Playlist_Maker;
$Playlist_Maker->plug_in();
register_activation_hook( __FILE__, array(&$Playlist_Maker, 'activate') );
register_deactivation_hook( __FILE__, array(&$Playlist_Maker, 'deactivate') );

/**
 * Template tag: Get hyperlink for a playlist
 *
 * @param int $id
 * @param string $type
 * @param string $contents Contents of the hyperlink
 * @return bool|string HTML hyperlink or false on failure
 */
function get_playlist_link( $id = null, $type = PM_DEFAULT_PLAYLIST_FORMAT, $contents = null ) {
	return $GLOBALS['Playlist_Maker']->get_playlist_link( $id, $type, $contents );
}

/**
 * Template tag: Get URL of a playlist
 *
 * @param int $id
 * @param string $type
 * @return bool|string URL or false on failure
 */
function get_playlist_url( $id = null, $type = PM_DEFAULT_PLAYLIST_FORMAT ) {
	return $GLOBALS['Playlist_Maker']->get_playlist_url( $id, $type );
}

/**
 * Does everything
 *
 */
class Playlist_Maker
{
	var	$options = array(
			'playable_attachment_types' => array('audio', 'video'),
			'save_playlist_types' => array('m3u', 'xspf'),
			'tracks_orderby' => 'ID',
			'tracks_order' => 'DESC',
		),
		$types	= array( 'audio', 'video' ), // move to 'playable types' above !
		$save_types = null, // array('m3u', 'xspf'),
		$log = array(),
		$errors = null,
		$did_notices = false,
		##
		$mime = array(
			'm3u' => 'audio/x-mpegurl',
			'xspf' => 'application/xspf+xml',
			'smil' => 'application/smil'
			),
		##	Marked to save @ shutdown
		$save = array(
			'attachments' => array(),
			'posts' => array(),
			'all' => false,
			),
		/*
			TODO Some filters should be added conditionally!
		*/
		$actions = array(
			'add_attachment'	=>	'update_attachment',
			'edit_attachment'	=>	'update_attachment',
			'delete_attachment'	=>	'update_attachment',
			'edit_post'	=>	'update_post',
			'init' => 'init',
			'parse_query' => 'parse_query',
			'shutdown' => 'shutdown',
			'wp' => 'wp',
			'admin_notices' => 'error_notices',
			'admin_footer' => 'error_notices',
			),
		$query = null;
	function __construct() {
		$this->errors = new WP_Error;
	}
	function Playlist_Maker() {
		$this->__construct();
	}
	function plug_in() {
		foreach ( $this->actions as $hook => $method ) {
			add_action( $hook, array( &$this, $method ) );
		}
	}
	function init() {
		if ( PM_REWRITE ) {
			add_rewrite_endpoint( PM_REWRITE_ENDPOINT_NAME, PM_REWRITE_ENDPOINT_PLACES );
		}
	}
	function activate() {
		if ( PM_REWRITE ) {
			$GLOBALS['wp_rewrite']->flush_rules();
			$this->log('Flushing rewrite rules');
		}
		if ( $this->options['save_playlist_types'] ) {
			if ( ! file_exists(PM_CONTENT_PATH) ) {
				$created = mkdir(PM_CONTENT_PATH);
				if ( ! $created ) {
					$this->log("Couldn't create " . PM_CONTENT_PATH);
					$this->errors->add('mkdir_failed', __('Could not create directory'), PM_CONTENT_PATH);
					return;
				}
				$this->log('Created ' . PM_CONTENT_PATH);
			}
			// Content directory exists
			if ( ! is_writable(PM_CONTENT_PATH) ) {
				$this->errors->add('unwritable', "Can't write to this directory", PM_CONTENT_PATH);
				return;
			}
			// Should this be done @ shutdown?
			$this->save_all_playlists();
		}
	}
	function error_notices() {
		if ( $this->did_notices ) {
			return;
		}
		$msgs = $this->errors->get_error_messages();
		if ( $msgs ) {
			$msgs = array_map( 'htmlspecialchars', $msgs );
			echo '<ul id="pm-errors" class="error">';
			foreach ( $msgs as $code => $msg ) {
				echo '<li>' . $this->xmlspecialchars($msg);
				// Only 1 datum per error code, ever ??
				$data = $this->errors->get_error_data($code);
				if ($data) {
					echo '<var>' . $this->xmlspecialchars($data) . '</var>';
				}
				echo '</li>';
			}
			echo '</ul>';
		}
		$this->did_notices = true;
		if ( PM_LOG && $this->log ) {
			echo '<ul id="pm-log">';
			foreach ( $this->log as $i => $msg ) {
				echo '<li>' . Playlist_Maker::xmlspecialchars($msg) . '</li>';
			}
			echo '</ul>';
		}
	}
	function save_all_playlists() {
		// Get all parents of playable attachments; write playlists
		$attachments = $this->get_attachments( null, $this->options['playable_attachment_types'] );
		$this->save_playlists_all( $this->options['save_playlist_types'], $attachments );
		// arrange by parent
		foreach ( $attachments as $attachment ) {
			$parents[$attachment->post_parent][$attachment->ID] = $attachment;
		}
		// var_dump($parents);
		foreach ( $parents as $parent_id => $kids ) {
			$this->save_playlists_from_post( $parent_id, $this->options['save_playlist_types'], $kids );
		}
	}
	function deactivate() {
		$GLOBALS['wp_rewrite']->flush_rules();
	}
	function &get_attachments( $parent, $type = null, $count = -1 ) {
		isset($type)
			or $type = $this->options['playable_attachment_types'];
		$children =& get_children(array(
			'numberposts' => $count,
			'post_parent' => $parent,
			'post_type' => 'attachment',
			'post_mime_type' => $type,
		));
		$this->log("Got " .count($children). " attachments for parent $parent type " . implode(',', $type));
		return $children;
	}
	/**
	 * If query is for home/front page playlist, print playlist & exit
	 *
	 * Hook parse_query: Request type is known (front, single, attachment) but no posts are retrieved yet.
	 *
	 * @param object $wp_query
	 */
	function parse_query( &$wp_query ) {
		// parse_query called for get_posts() too; DONT get in a loop
		$play = get_query_var(PM_REWRITE_ENDPOINT_NAME);
		$this->log("play = '$play'");
		if ( 'default' == $play ) {
			$play = PM_DEFAULT_PLAYLIST_FORMAT;
			set_query_var(PM_REWRITE_ENDPOINT_NAME, $play);
			$this->log("Setting play to default: play=" . get_query_var(PM_REWRITE_ENDPOINT_NAME));
		}
		if ( $play
			&& empty($wp_query->query_vars['suppress_filters'])
			&& $wp_query === $GLOBALS['wp_the_query'] )
		{
			$this->log("This is a playlist query {$wp_query->request}");
			$this->query =& $wp_query; // kepp track of the query
			if ( ! is_single() && ! is_page() ) {
				set_query_var('post_type', 'attachment');
				set_query_var('post_status', 'inherit');
				set_query_var('post_mime_type', $this->options['playable_attachment_types']);
			}
		}
	}
	/**
	 * Send playlist for Pages and posts; exit
	 *
	 * Hook wp: Posts have been loaded
	 *
	 * @param object $wp
	 * @author Sam
	 */

	function wp( &$wp ) {
		$play = get_query_var(PM_REWRITE_ENDPOINT_NAME);
		if ( $play ) {
			$this->log("This is a playlist request");
			// $this->log( $this->query );
			if ( is_404() ) {
				// ??
			} else {
				if ( is_page() || is_single() ) {
					// global $id is not set yet; get_the_ID() doesn't work
					if ( empty($GLOBALS['id']) && isset($GLOBALS['wp_the_query']->post->ID) ) {
						$GLOBALS['id'] = $GLOBALS['wp_the_query']->post->ID;
					}
					$attachments =& $this->get_attachments( get_the_ID(), $this->options['playable_attachment_types'] );
				} else {
					$attachments =& $GLOBALS['wp_the_query']->posts;
				}
				if ( $attachments ) {
					$this->send_playlist( $play, $attachments );
				} else {
					$this->send_404("No playable tracks");
				}

				$this->log('Exiting at ' . __CLASS__ . '->' . __FUNCTION__);
				exit;
			}

		}
	}
	function send_playlist( $type, &$attachments ) {
		$this->log("Send playlist type '$type'");
		switch ($type) {
			case 'm3u':
				header('Content-Type: application/x-mpegurl; charset='.get_bloginfo('charset'));
				header('Content-Disposition: inline; filename=' . $this->get_the_playlist_filename('m3u') );
				header('Cache-Control: max-age='.PM_PLAYLIST_MAX_AGE );
				$this->print_playlist('m3u', $attachments);
				break;
			case 'xspf':
				header('Content-Type: application/xspf+xml; charset='.get_bloginfo('charset'));
				header('Content-Disposition: inline; filename=' . $this->get_the_playlist_filename('xspf') );
				header('Cache-Control: max-age='.PM_PLAYLIST_MAX_AGE );
				$this->print_playlist('xspf', $attachments);
				break;
			case 'smil':
				header('Content-Type: application/smil');
				$this->print_playlist('smil', $attachments);
				break;
			default:
				$this->log("Can't send playlist type '$type', what is it?");
				$type = wp_specialchars($type);
				Playlist_Maker::send_404("I don't recognize playlist type <var>$type</var>");
				break;
		}
	}
	function send_404( $msg ) {
		status_header(404);
		// get_header();
		echo '<h1>Not found</h1>' . "<p>$msg</p>";
		// get_footer();
	}
	function print_playlist($type, &$attachments) {
		$this->log("Print playlist type '$type'");
		switch ($type) {
			case 'atom':
				include 'templates/atom.php';
				break;
			case 'm3u':
				foreach ( $attachments as $attachment ) {
					echo wp_get_attachment_url( $attachment->ID ) . "\n";
				}
				break;
			case 'xspf':
				$display_errors = ini_set('display_errors', 0); // don't break XML
				include 'templates/xspf.php';
				PM_LOG && Playlist_Maker::comment($this->log);
				ini_set( 'display_errors', $display_errors );
				break;
			case 'smil':
				$display_errors = ini_set('display_errors', 0); // don't break XML
				include 'templates/smil.php';
				PM_LOG && Playlist_Maker::comment($this->log);
				ini_set( 'display_errors', $display_errors );
				break;
			default:
				$this->log('Nothing happened');
				break;
		}
	}
	function get_playlist( $type, &$attachments ) {
		$this->log("get playlist type $type with " . count($attachments) . ' tracks');
		switch ($type) {
			case 'm3u':
				return implode( "\n"
					, array_map( 'wp_get_attachment_url', array_keys($attachments) ));
				break;
			case 'xspf':
				ob_start();
				include 'templates/xspf.php';
				PM_LOG && Playlist_Maker::comment($this->log);
				return ob_get_clean();
				break;
			case 'smil':
				ob_start();
				include 'templates/smil.php';
				PM_LOG && Playlist_Maker::comment($this->log);
				return ob_get_clean();
				break;
			default:
				trigger_error("Unknown playlist type $type", E_USER_WARNING);
				break;
		}
	}
	/**
	 * Get string for XML element
	 *
	 * @param string $name Name of XML element
	 * @param array $attributes
	 * @param array|string $contents
	 * @return string XML chunk
	 * @author Sam
	 */

	function get_XML_element( $name, $attributes = null, $contents = null ) {
		$output = "<$name";
		if ( $attributes ) {
			foreach ( $attributes as $attr => $value ) {
				$output .= " $attr=\"" . Playlist_Maker::xmlspecialchars($value) . '"';
			}
		}
		if ( empty($contents) ) {
			$output .= '/>';
		} else {
			$output .= '>';
			if ( is_string($contents) ) {
				$output .= Playlist_Maker::xmlspecialchars($contents);
			}
			elseif ( is_array($contents) ) {
				$output .= implode( "\n", $contents );
			}
			$output .= "</$name>";
		}
		// strip invalid chars only
		$output = Playlist_Maker::xmlf($output, false); 
		return $output;
	}
	/**
	 * Return value for an element in XSPF <playlist>, or some other playlist type
	 *
	 * @param string $name Name of XSPF playlist element ('title', 'info', 'image')
	 * @param bool $xml Format for XML?
	 * @return string
	 * @todo What is the best way to XML-escape these?
	 * @author Sam
	 */

	function get_playlist_value( $name, $xml = false ) {
		// use add_filter( 'pm_playlist_value_pre', $callback, $priority, 2 );
		// function callback( $empty, $name ) {...}
		$output = apply_filters( 'pm_playlist_value_pre', '', $name );
		if ( empty($output) ) {
			switch ($name) {
				case 'title':
					return wp_title( '', false, 'right' ) . get_bloginfo('name'); // needs XML formatting
					break;
				case 'info':
					if (is_single())
						return apply_filters('the_permalink_rss', get_permalink());
					elseif (is_category())
						return apply_filters('the_permalink_rss', get_category_link() );
					else
						// for other requests, should be able to figure this out from query
						return get_bloginfo_rss('url'); // lame
					break;
				case 'image':
					if ( is_single() || is_page() ) {
						// return get_post_meta 'image' first; else:
						$images = get_children(array(
							'numberposts' => 1,
							'post_parent' => $this->query->post->ID, // too early for get_the_ID()
							'post_type' => 'attachment', // required in WordPress < 2.7
							'post_mime_type' => 'image',
							));
						if ( $images ) {
							$image = current($images);
							return wp_get_attachment_url($image->ID);
						}
					} else {
						// no image
					}
					break;
				case 'creator':
					if ( is_singular() ) {
						return get_the_author();
					}
					break;
				default:
					trigger_error( __FUNCTION__ . ": Can't output value $name", E_USER_NOTICE );
					break;
			}
		}
		if ($output && $xml) {
			$output = Playlist_Maker::xmlf($output, true);
		}
		return $output;
	}
	/**
	 * Get value for element of XPSF <track>, or other
	 *
	 * @param string $name Name of the element (title, location, annotation, info, creator, album)
	 * @param object $attachment Attachment representing the track
	 * @param bool $xml Format for XML?
	 * @return string value
	 * @author Sam
	 */

	function get_track_value( $name, &$attachment, $xml = false ) {
		// use add_filter( 'pm_track_value_pre', $callback, $priority, 2 );
		// function callback( $empty, $name, $attachment ) {...}
		$output = apply_filters( 'pm_track_value_pre', '', $name, $attachment );
		if ( empty($output) ) {
			switch ($name) {
			case 'title':
				$output = strip_tags($attachment->post_title);
				break;
			case 'location':
				$output = wp_get_attachment_url($attachment->ID);
				break;
			case 'annotation':
				$output = strip_tags($attachment->post_content);
				break;
			case 'info':
				$output = get_permalink($attachment->post_parent);
				break;
			default:
				trigger_error( __FUNCTION__ . ": Unknown value $name", E_USER_NOTICE );
				break;
			}
		}
		// $this->log("$name: $output");
		if ($output && $xml) {
			$output = Playlist_Maker::xmlf($output, true);
		}
		return $output;
	}

	/**
	 * Get title of the playlist for the current request
	 *
	 * @param string $sep
	 * @return string title
	 * @author Sam
	 */
	function get_the_playlist_title( $sep = '' ) {
		return wp_title( '', false, 'right' ) . get_bloginfo('name');
	}

	/**
	 * Get filename of the playlist for the current request
	 *
	 * @param string $ext
	 * @return string filename
	 * @author Sam
	 */
	function get_the_playlist_filename( $ext ) {
		return Playlist_Maker::sanitize_filename( $this->get_the_playlist_title() . ".$ext" );
	}
	/**
	 * Get filename of a playlist for post, Page, or 'all'; optional filename extension
	 *
	 * @param string $id
	 * @param string $type
	 * @return void
	 * @author Sam
	 */

	function get_playlist_filename( $id, $type = null ) {
		$ext = empty($type) ? '' : ".$type";
		if ( -1 == $id ) {
			// "all" playlist
			return Playlist_Maker::sanitize_filename(PM_CONTENT_ALL_NAME . $ext);
		} else {
			// post/Page playlist
			$post =& get_post($id);
			if ( ! $post ) {
				trigger_error(__FUNCTION__.": can't find post id $id");
				return false;
			}
			return Playlist_Maker::sanitize_filename($post->post_name . $ext);
		}
	}
	function get_playlist_link( $id = null, $type = PM_DEFAULT_PLAYLIST_FORMAT, $contents = null ) {
		$url = $this->get_playlist_url( $id, $type );
		if ( $url ) {
			if ( empty($contents) ) {
				$post =& get_post($id);
				$contents = $post->post_title;
			}
			return Playlist_Maker::get_XML_element( 'a', array(
				'href' => $url,
				'type' => $this->mime[$type] )
				, $contents );
		} else {
			return false;
		}

	}

	/**
	 * Get URL for a post, Page, or 'all' playlist
	 *
	 * @param integer $id Post/Page ID or -1 for all
	 * @param string $type Playlist type
	 * @return bool|string Playlist URL or false
	 * @author Sam
	 */
	function get_playlist_url( $id = null, $type = PM_DEFAULT_PLAYLIST_FORMAT ) {

		$this->log("id=$id, type=$type", __FUNCTION__);
		if ( $this->save_types && in_array( $type, $this->save_types ) ) {
			$this->log("$type is a saved type, look for a playlist file", __FUNCTION__);
			$filename = $this->get_playlist_filename( $id, $type );
			if ( $filename ) {
				$this->log("Look for filename $filename");
				$path = PM_CONTENT_PATH . DIRECTORY_SEPARATOR . $filename;
				$this->log("Look for $path");
				if ( file_exists($path) ) {
					$url = PM_CONTENT_URL . $filename;
					$this->log("Found file '$path'; returning '$url'", __FUNCTION__);
					return $url;
				} else {
					$this->log("Expected to find saved file $path but it's not there", __FUNCTION__);
					trigger_error(__FUNCTION__ . " : No such file '$path'");
					// continue
				}
			} else {
				$this->log("Couldn't get playlist filename for id=$id");
			}
		}
		if ( -1 == $id ) {
			$url = get_bloginfo('url');
			$this->log("'all' playlist under $url");
		} else {
			$url = get_permalink($id);
		}

		if ( ! $url ) {
			trigger_error("Couldn't make URL from post ID $id");
			return false;
		}
		if ( $GLOBALS['wp_rewrite']->using_permalinks() ) {
			if ( PM_REWRITE ) {
				$url = user_trailingslashit( trailingslashit($url) . PM_REWRITE_ENDPOINT_NAME . "/$type" );
			} else {
				// post_url should have no query string, i hope
				$url = $url . '?' . PM_REWRITE_ENDPOINT_NAME . "=$type";
			}
		} else {
			$url = $url . '&' . PM_REWRITE_ENDPOINT_NAME . "=$type";
		}
		$this->log("Returning '$url'");
		return $url;
	}

	/**
	 * If attachment is playable, add parent to our queue
	 *
	 * @param integer $id Attachment ID
	 */

	function update_attachment( $id ) {
		if ( ! PM_CONTENT )
			return;
		$attachment =& get_post($id);
		if ( $this->is_playable($attachment) ) {
			$this->save_playlists_from_post( $attachment->post_parent );
			$this->save['posts'][] = $attachment->post_parent;
			$this->save['all'] = true;
		}
	}
	function update_post( $id ) {
		// it's easier to mark all posts and then check for playable attachments in save_playlists_from_post()
		$this->save['posts'][] = $id;
	}
	/**
	 * Save playlist files of each format for a post or Page
	 *
	 * @param integer $id
	 * @param array|string $types
	 * @author Sam
	 */
	function save_playlists_from_post( $id, $types )
	{
		return $this->save_playlists_from_attachments(
			Playlist_Maker::get_playlist_filename($id)
			, $this->get_attachments( $id, $this->options['playable_attachment_types'] )
			, $types
		);
	}
	function save_playlists_from_attachments( $name, & $attachments, $types ) {
		$result = false;
		if ( $attachments ) {
			foreach ( (array) $types as $type ) {
				$saved = $this->save_playlist( "$name.$type"
					, $this->get_playlist($type, $attachments) );
				$result = $result && $saved;
			}
		}
		return $result;
	}
	/**
	 * Write a playlist file to the content directory
	 *
	 * @param string $filename
	 * @param string $data
	 * @return integer|object bytes saved or PEAR error
	 * @author Sam
	 */

	function save_playlist( $filename, $data ) {
		$bytes = strlen($data);
		$filename = Playlist_Maker::sanitize_filename($filename);
		$path = PM_CONTENT_PATH . DIRECTORY_SEPARATOR . $filename;
		// $this->log("Save $path ($bytes bytes)...");
		$result = Playlist_Maker::write( $path, $data );
		if( $result ) {
			$this->log("Saved playlist $path ($result bytes)");
			return $result;
		} else {
			$this->log("Couldn't save playlist '$path'");
			$this->errors->add( 'save_failed', "Couldn't save playlist", $path);
			return false;
		}
	}
	/**
     * Writes the given data to the given filename.
     *
     * @access  public
     * @param   string  $filename Path of file to write to
     * @param   string  $data Data to write to file
     * @param   string  $start Mode to open file in
     * @return  mixed   false on error or number of bytes written to file.
     */
    function write($path, $data, $mode = 'w')
    {
		$fp = fopen( $path, $mode );
		if ( ! $fp ) {
			$this->errors->add( 'fopen', "Can't open file pointer", $path);
			return false;
		}

		if (false === $bytes = @fwrite($fp, $data)) {
			$this->errors->add( 'fwrite', "Can't write to file", $path);
			return false;
		}
		return $bytes;
    }
	/**
	 * Save playlist files ('all.xspf', 'all.m3u') of each format for all playable attachments
	 *
	 * @param string $types
	 * @param string $attachments
	 * @return void
	 * @author Sam
	 */

	function save_playlists_all( $types, $attachments = null )
	{
		isset($attachments)
			or $attachments =& $this->get_attachments( null, $this->options['playable_attachment_types']);
		$this->log( 'Save playlist ' . PM_CONTENT_ALL_NAME . ' ' . implode(',', $types));

		if ( $attachments ) {
			return $this->save_playlists_from_attachments( PM_CONTENT_ALL_NAME, $attachments, $types );
		} else {
			trigger_error('No attachments to save');
			return false;
		}
	}
	function shutdown()
	{
		$this->log("Queue has " . count($this->save['posts']) . " posts");
		foreach ( array_unique($this->save['posts']) as $post_id ) {
			$this->save_playlists_from_post( $post_id, $this->options['save_playlist_types'] );
		}
		$this->log("Save playlist 'all'? " . ( $this->save['all'] ? 'yes' : 'no' ));
		if ($this->save['all']) {
			$this->save_playlists_all($this->options['save_playlist_types']);
		}
		$this->log("Finished shutdown");
		// Printing @ shutdown can spoil XML
		// or stop redirects during post save!
		if (PM_DEBUG) var_dump($this);
	}
	function is_playable( $thing ) {
		if ( is_object($thing) && !empty($thing->post_mime_type) ) {
			$thing = $thing->post_mime_type;
		}
		$types = str_replace('%', '', $this->options['playable_attachment_types']);
		$types = implode(',', $types);
		$types = preg_quote($types);

		return (bool) preg_match( "@($types)@", $thing );
	}
	function comment( $s ) {
		if ( ! is_scalar($s) ) {
			$s = print_r($s, true);
		}
		echo '<!-- ' . preg_replace('/--+/', '', $s ) . ' -->';
	}
	function log( $msg ) {
		if ( ! PM_LOG)
			return;
		$trace = debug_backtrace();
		$trace = $trace[1];
		$source = $trace['function'] . ' ';
		if ( isset($GLOBALS['wp_current_filter'][0]) ) {
			$source .= "({$GLOBALS['wp_current_filter'][0]}) ";
		}
		if (! is_string($msg))
			$msg = print_r($msg, true);
		$this->log[] = $source . $msg;
	}
	function xmlspecialchars( $string, $quotes = ENT_QUOTES, $charset = PM_BLOG_CHARSET, $double_encode = null ) {
		return htmlspecialchars( $string, $quotes, $charset );
	}
	/**
	 * Strip invalid XML characters; optionally encode special characters
	 *
	 * @param string $string
	 * @return string
	 * @link http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char XML character range
	 * @link http://www.w3.org/International/questions/qa-controls#support How do I handle control codes in XML, XHTML and HTML?
	 */
	function xmlf( $string, $specialchars = false, $quotes = ENT_QUOTES, $charset = PM_BLOG_CHARSET, $double_encode = null )
	{
		if ( $specialchars ) {
			$string = Playlist_Maker::xmlspecialchars( $string, $quotes, $charset, $double_encode );
		}
		$string = preg_replace( '/['
			  . '\x00-\x08'	# C0 controls except horz tab, line feed, carriage return, space
			  . '\x0B-\x0C'
			  . '\x0E-\x1F'
			  . '\x7F-\x9F'	# C1 controls (illegal in HTML, X(H)TML 1)
			. ']+/', '', $string );
		return $string;
	}
	function sanitize_filename($s) {
		$s = strtolower($s);
		return preg_replace('/[^a-z0-9._-]+/', '_', $s);
	}
}

?>