View file upload/includes/class_dm_attachment.php

File size: 32.95Kb
<?php
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.0.5
|| # ---------------------------------------------------------------- # ||
|| # Copyright ©2000-2010 vBulletin Solutions Inc. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
|| #################################################################### ||
\*======================================================================*/

if (!class_exists('vB_DataManager', false))
{
	exit;
}

// Temporary
require_once(DIR . '/includes/functions_file.php');

/**
* Abstract class to do data save/delete operations for ATTACHMENTS.
*
* @package	vBulletin
* @version	$Revision: 35688 $
* @date		$Date: 2010-03-04 15:40:16 -0800 (Thu, 04 Mar 2010) $
*/
abstract class vB_DataManager_AttachData extends vB_DataManager
{
	/**
	* Array of field names that are bitfields, together with the name of the variable in the registry with the definitions.
	*
	* @var	array
	*/
	var $bitfields = array();

	/**
	* Storage holder
	*
	* @var  array   Storage Holder
	*/
	var $lists = array();

	/**
	* Storage Type
	*
	* @var  string
	*/
	var $storage = 'db';

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	public function __construct(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::vB_DataManager($registry, $errtype);

		$this->storage = $registry->options['attachfile'] ? 'fs' : 'db';

		($hook = vBulletinHook::fetch_hook('attachdata_start')) ? eval($hook) : false;
	}

	/**
	* Set the extension of the filename
	*
	* @param	filename
	*
	* @return	boolean
	*/
	function verify_filename(&$filename)
	{
		$ext_pos = strrpos($filename, '.');
		if ($ext_pos !== false)
		{
			$extension = substr($filename, $ext_pos + 1);
			// 100 (filename length in DB) - 1 (.) - length of extension
			$filename = substr($filename, 0, min(100 - 1 - strlen($extension), $ext_pos)) . ".$extension";
		}
		else
		{
			$extension = '';
		}

		if ($this->validfields['extension'])
		{
			$this->set('extension', strtolower($extension));
		}
		return true;
	}

	/**
	* Set the filesize of the thumbnail
	*
	* @param	integer	Maximum posts per page
	*
	* @return	boolean
	*/
	function verify_thumbnail(&$thumbnail)
	{
		if (strlen($thumbnail) > 0)
		{
			$this->set('thumbnail_filesize', strlen($thumbnail));
		}
		return true;
	}

	/**
	* Set the filehash/filesize of the file
	*
	* @param	integer	Maximum posts per page
	*
	* @return	boolean
	*/
	function verify_filedata(&$filedata)
	{
		if (strlen($filedata) > 0)
		{
			$this->set('filehash', md5($filedata));
			$this->set('filesize', strlen($filedata));
		}

		return true;
	}

	/**
	* Verify that posthash is either md5 or empty
	* @param	string the md5
	*
	* @return	boolean
	*/
	function verify_md5_alt(&$md5)
	{
		return (empty($md5) OR (strlen($md5) == 32 AND preg_match('#^[a-f0-9]{32}$#', $md5)));
	}

	/**
	* database pre_save method that only applies to subclasses that have filedata fields
	*
	* @param	boolean	Do the query?
	*
	* @return	boolean	True on success; false if an error occurred
	*/
	function pre_save_filedata($doquery = true)
	{
		if ($this->condition === null)
		{
			if ($this->fetch_field('filehash', 'filedata'))
			{
				$filehash = $this->fetch_field('filehash', 'filedata');
			}
			else if (!empty($this->info['filedata_location']) AND file_exists($this->info['filedata_location']))
			{
				$filehash = md5(file_get_contents($this->info['filedata_location']));
			}
			else if (!empty($this->info['filedata']))
			{
				$filehash = md5($this->info['filedata']);
			}
			else if ($this->fetch_field('filedata', 'filedata'))
			{
				$filehash = md5($this->fetch_field('filedata', 'filedata'));
			}

			// Does filedata already exist?
			if ($filehash AND $fd = $this->registry->db->query_first("
				SELECT filedataid
				FROM " . TABLE_PREFIX . "filedata
				WHERE filehash = '" . $this->registry->db->escape_string($filehash) . "'
			"))
			{
				// file already exists so we are not going to insert a new one
				return $fd['filedataid'];
			}
		}

		if ($this->storage == 'db')
		{
			if (!empty($this->info['filedata_location']) AND file_exists($this->info['filedata_location']))
			{
				$this->set_info('filedata', file_get_contents($this->info['filedata_location']));
			}

			if (!empty($this->info['filedata']))
			{
				$this->setr('filedata', $this->info['filedata']);
			}

			if (!empty($this->info['thumbnail']))
			{
				$this->setr('thumbnail', $this->info['thumbnail']);
			}
		}
		else	// Saving in the filesystem
		{
			// make sure we don't have the binary data set
			// if so move it to an information field
			// benefit of this is that when we "move" files from DB to FS,
			// the filedata/thumbnail fields are not blanked in the database
			// during the update.
			if ($file =& $this->fetch_field('filedata', 'filedata'))
			{
				$this->setr_info('filedata', $file);
				$this->do_unset('filedata', 'filedata');
			}

			if ($thumb =& $this->fetch_field('thumbnail', 'filedata'))
			{
				$this->setr_info('thumbnail', $thumb);
				$this->do_unset('thumbnail', 'filedata');
			}

			if (!empty($this->info['filedata']))
			{
				$this->set('filehash', md5($this->info['filedata']), true, true, 'filedata');
				$this->set('filesize', strlen($this->info['filedata']), true, true, 'filedata');
			}
			else if (!empty($this->info['filedata_location']) AND file_exists($this->info['filedata_location']))
			{
				$this->set('filehash', md5_file($this->info['filedata_location']), true, true, 'filedata');
				$this->set('filesize', filesize($this->info['filedata_location']), true, true, 'filedata');
			}

			if (!empty($this->info['thumbnail']))
			{
				$this->set('thumbnail_filesize', strlen($this->info['thumbnail']), true, true, 'filedata');
			}

			if (!empty($this->info['filedata']) OR !empty($this->info['thumbnail']) OR !empty($this->info['filedata_location']))
			{
				$path = $this->verify_attachment_path($this->fetch_field('userid', 'filedata'));
				if (!$path)
				{
					$this->error('attachpathfailed');
					return false;
				}

				if (!is_writable($path))
				{
					$this->error('upload_file_system_is_not_writable_path', htmlspecialchars($path));
					return false;
				}
			}
		}

		return true;
	}

	/**
	* Additional data to update after a save call (such as denormalized values in other tables).
	* In batch updates, is executed for each record updated.
	*
	* @param	boolean	Do the query?
	*/
	function post_save_each_filedata($doquery = true)
	{
		if ($this->storage == 'fs')
		{
			$filedataid =& $this->fetch_field('filedataid', 'filedata');
			$userid =& $this->fetch_field('userid', 'filedata');
			$failed = false;

			// Check for filedata in an information field
			if (!empty($this->info['filedata']))
			{
				$filename = fetch_attachment_path($userid, $filedataid);
				if ($fp = fopen($filename, 'wb'))
				{
					if (!fwrite($fp, $this->info['filedata']))
					{
						$failed = true;
					}
					fclose($fp);
					#remove possible existing thumbnail in case no thumbnail is written in the next step.
					if (file_exists(fetch_attachment_path($userid, $filedataid, true)))
					{
						@unlink(fetch_attachment_path($userid, $filedataid, true));
					}
				}
				else
				{
					$failed = true;
				}
			}
			else if (!empty($this->info['filedata_location']))
			{
				$filename = fetch_attachment_path($userid, $filedataid);
				if (@rename($this->info['filedata_location'], $filename))
				{
					$mask = 0777 & ~umask();
					@chmod($filename, $mask);

 					if (file_exists(fetch_attachment_path($userid, $filedataid, true)))
					{
						@unlink(fetch_attachment_path($userid, $filedataid, true));
					}
				}
				else
				{

					$failed = true;
				}
			}

			if (!$failed AND !empty($this->info['thumbnail']))
			{
				// write out thumbnail now
				$filename = fetch_attachment_path($userid, $filedataid, true);
				if ($fp = fopen($filename, 'wb'))
				{
					if (!fwrite($fp, $this->info['thumbnail']))
					{
						$failed = true;
					}
					fclose($fp);
				}
				else
				{
					$failed = true;
				}
			}

			($hook = vBulletinHook::fetch_hook('attachdata_postsave')) ? eval($hook) : false;

			if ($failed)
			{
				if ($this->condition === null) // Insert, delete filedata
				{
					$this->registry->db->query_write("
						DELETE FROM " . TABLE_PREFIX . "filedata
						WHERE filedataid = $filedataid
					");
					$this->registry->db->query_write("
						DELETE FROM " . TABLE_PREFIX . "attachmentcategoryuser
						WHERE filedataid = $filedataid
					");
				}

				// $php_errormsg is automatically set if track_vars is enabled
				$this->error('upload_copyfailed', htmlspecialchars_uni($php_errormsg), fetch_attachment_path($userid));
				return false;
			}
			else
			{
				return true;
			}
		}
	}

	/**
	* Any code to run before deleting.
	*
	* @param	string	What are we deleteing?
	*/
	function pre_delete($type = 'attachment', $doquery = true, $checkperms = true)
	{
		$this->lists['content'] = array();
		$this->lists['filedataids'] = array();
		$this->lists['attachmentids'] = array();
		$this->lists['picturecomments'] = array();
		$this->lists['userids'] = array();
		$this->set_info('type', $type);

		if ($type == 'filedata')
		{
			$ids = $this->registry->db->query_read("
				SELECT a.attachmentid, fd.userid, fd.filedataid, a.userid AS auserid, a.contenttypeid
				FROM " . TABLE_PREFIX . "filedata AS fd
				LEFT JOIN " . TABLE_PREFIX . "attachment AS a ON (a.filedataid = fd.filedataid)
				WHERE " . $this->condition
			);
		}
		else
		{
			$ids = $this->registry->db->query_read("
				SELECT a.attachmentid, fd.userid, fd.filedataid, a.userid AS auserid, a.contenttypeid
				FROM " . TABLE_PREFIX . "attachment AS a
				LEFT JOIN " . TABLE_PREFIX . "filedata AS fd ON (a.filedataid = fd.filedataid)
				WHERE " . $this->condition
			);
		}
		while ($id = $this->registry->db->fetch_array($ids))
		{
			if ($id['attachmentid'])
			{
				$this->lists['content']["$id[contenttypeid]"][] = $id['attachmentid'];
				$this->lists['attachmentids'][] = $id['attachmentid'];
				$this->lists['picturecomments'][] = "(filedataid = $id[filedataid] AND userid = $id[auserid])";
				$this->lists['userids']["$id[auserid]"] = 1;
			}
			if ($id['filedataid'])
			{
				$this->lists['filedataids']["$id[filedataid]"] = $id['userid'];
			}
		}

		require_once(DIR . '/packages/vbattach/attach.php');
		if ($this->registry->db->num_rows($ids) == 0)
		{	// nothing to delete
			return false;
		}
		else
		{
			foreach ($this->lists['content'] AS $contenttypeid => $list)
			{
				if (!($attach =& vB_Attachment_Dm_Library::fetch_library($this->registry, $contenttypeid)))
				{
					return false;
				}
				if (!$attach->pre_delete($list, $checkperms, $this))
				{
					return false;
				}
				unset($attach);
			}
		}

		return parent::pre_delete($doquery);
	}

	/**
	* Additional data to update after a save call (such as denormalized values in other tables).
	* In batch updates, is executed for each record updated.
	*
	* @param	boolean	Do the query?
	*/
	function post_save_each($doquery = true)
	{
		if ($contenttypeid = intval($this->fetch_field('contenttypeid')))
		{
			require_once(DIR . '/packages/vbattach/attach.php');
			if (!($attach =& vB_Attachment_Dm_Library::fetch_library($this->registry, $contenttypeid)))
			{
				return false;
			}
			$attach->post_save_each($this);
		}
		return parent::post_save_each($doquery);
	}

	/**
	* Any code to run after deleting
	*
	* @param	Boolean Do the query?
	*/
	function post_delete($doquery = true)
	{
		foreach ($this->lists['content'] AS $contenttypeid => $list)
		{
			if (!($attach =& vB_Attachment_Dm_Library::fetch_library($this->registry, $contenttypeid)))
			{
				return false;
			}
			$attach->post_delete($this);
			unset($attach);
		}
		// Update the refcount in the filedata table
		if (!empty($this->lists['filedataids']))
		{
			$this->registry->db->query_write("
				UPDATE " . TABLE_PREFIX . "filedata AS fd
				SET fd.refcount = (
					SELECT COUNT(*)
					FROM " . TABLE_PREFIX . "attachment AS a
					WHERE fd.filedataid = a.filedataid
				)
				WHERE fd.filedataid IN (" . implode(", ", array_keys($this->lists['filedataids'])) . ")
			");
		}
		// Hourly cron job will clean out the FS where refcount = 0 and dateline > 1 hour

		// Below here only applies to attachments in pictures/groups but I forsee all attachments gaining the ability to have comments
		if ($this->info['type'] == 'filedata')
		{
			if (!empty($this->lists['filedataids']))
			{
				$this->registry->db->query_write("
					DELETE FROM " . TABLE_PREFIX . "picturecomment
					WHERE filedataid IN (" . implode(", ", array_keys($this->lists['filedataids'])) . ")
				");
			}
		}
		else if (!empty($this->lists['picturecomments']))	// deletion type is by attachment
		{
			foreach ($this->lists['picturecomments'] AS $sql)
			{
				if (!($results = $this->registry->db->query_first("
					SELECT a.attachmentid
					FROM " . TABLE_PREFIX . "attachment AS a
					WHERE
						$sql
				")))
				{
					$this->registry->db->query_write("
						DELETE FROM " . TABLE_PREFIX . "picturecomment
						WHERE
							$sql
					");
				}
			}
		}

		require_once(DIR . '/includes/functions_picturecomment.php');
		foreach (array_keys($this->lists['userids']) AS $userid)
		{
			build_picture_comment_counters($userid);
		}

		return parent::post_delete($doquery);
	}

	/**
	* Verify that user's attach path exists, create if it doesn't
	*
	* @param	int		userid
	*/
	function verify_attachment_path($userid)
	{
		// Allow userid to be 0 since vB2 allowed guests to post attachments
		$userid = intval($userid);

		$path = fetch_attachment_path($userid);
		if (vbmkdir($path))
		{
			return $path;
		}
		else
		{
			return false;
		}
	}
}

/**
* Class to do data save/delete operations for just the Attachment table
*
* @package	vBulletin
* @version	$Revision: 35688 $
* @date		$Date: 2010-03-04 15:40:16 -0800 (Thu, 04 Mar 2010) $
*/

class vB_DataManager_Attachment extends vB_DataManager_AttachData
{
	/**
	* Array of recognized and required fields for attachment inserts
	*
	* @var	array
	*/
	var $validfields = array(
		'attachmentid'   => array(TYPE_UINT,       REQ_INCR),
		'filedataid'     => array(TYPE_UINT,       REQ_YES),
		'userid'         => array(TYPE_UINT,       REQ_YES),
		'filename'       => array(TYPE_STR,        REQ_YES,  VF_METHOD, 'verify_filename'),
		'dateline'       => array(TYPE_UNIXTIME,   REQ_AUTO),
		'state'          => array(TYPE_STR,        REQ_NO),
		'counter'        => array(TYPE_UINT,       REQ_NO),
		'posthash'       => array(TYPE_STR,        REQ_NO,   VF_METHOD, 'verify_md5_alt'),
		'contenttypeid'  => array(TYPE_UINT,       REQ_YES),
		'contentid'      => array(TYPE_UINT,       REQ_NO),
		'caption'        => array(TYPE_NOHTMLCOND, REQ_NO),
		'reportthreadid' => array(TYPE_UINT,       REQ_NO),
		'displayorder'   => array(TYPE_UINT,       REQ_NO),
	);

	/**
	* The main table this class deals with
	*
	* @var	string
	*/
	var $table = 'attachment';

	/**
	* Condition template for update query
	* This is for use with sprintf(). First key is the where clause, further keys are the field names of the data to be used.
	*
	* @var	array
	*/
	var $condition_construct = array('attachmentid = %1$d', 'attachmentid');

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	public function __construct(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::__construct($registry, $errtype);

		($hook = vBulletinHook::fetch_hook('attachdata_start')) ? eval($hook) : false;
	}

	public function pre_delete($doquery = true, $checkperms = true)
	{
		return parent::pre_delete('attachment', $doquery, $checkperms);
	}

	/**
	* Delete from the attachment table
	*
	*/
	public function delete($doquery = true, $checkperms = true)
	{
		if (!$this->pre_delete($doquery, $checkperms) OR empty($this->lists['attachmentids']))
		{
			return false;
		}

		$this->registry->db->query_write("
			DELETE FROM " . TABLE_PREFIX . "attachment
			WHERE attachmentid IN (" . implode(", ", $this->lists['attachmentids']) . ")
		");

		$this->post_delete($doquery);

		return true;
	}

	/**
	* Any code to run before approving
	*
	* @param	bool	Verify permissions
	*/
	function pre_moderate($checkperms = true, $type = 'approve')
	{
		$this->lists['content'] = array();
		$this->lists['attachmentids'] = array();
		$this->lists['userids'] = array();

		$ids = $this->registry->db->query_read("
			SELECT a.attachmentid, a.userid AS auserid, a.contenttypeid
			FROM " . TABLE_PREFIX . "attachment AS a
			WHERE " . $this->condition . "
		");
		while ($id = $this->registry->db->fetch_array($ids))
		{
			if ($id['attachmentid'])
			{
				$this->lists['content']["$id[contenttypeid]"][] = $id['attachmentid'];
				$this->lists['attachmentids'][] = $id['attachmentid'];
				$this->lists['userids']["$id[auserid]"] = 1;
			}
		}

		require_once(DIR . '/packages/vbattach/attach.php');
		if ($this->registry->db->num_rows($ids) == 0)
		{	// nothing to approve
			return false;
		}
		else
		{
			foreach ($this->lists['content'] AS $contenttypeid => $list)
			{
				if (!($attach =& vB_Attachment_Dm_Library::fetch_library($this->registry, $contenttypeid)))
				{
					return false;
				}
				if ($type == 'approve')
				{
					if (!$attach->pre_approve($list, $checkperms, $this))
					{
						return false;
					}
				}
				else
				{
					if (!$attach->pre_unapprove($list, $checkperms, $this))
					{
						return false;
					}
				}
				unset($attach);
			}
		}

		return true;
	}

	/**
	* Approve in the attachment table
	*
	*/
	public function approve($checkperms = true)
	{
		if (!$this->pre_moderate($checkperms, 'approve') OR empty($this->lists['attachmentids']))
		{
			return false;
		}

		$this->registry->db->query_write("
			UPDATE " . TABLE_PREFIX . "attachment
			SET state = 'visible'
			WHERE attachmentid IN (" . implode(", ", $this->lists['attachmentids']) . ")
		");

		$this->post_moderate('approve');

		return true;
	}

	/**
	* Unapprove in the attachment table
	*
	*/
	public function unapprove($checkperms = true)
	{
		if (!$this->pre_moderate($checkperms, 'unapprove') OR empty($this->lists['attachmentids']))
		{
			return false;
		}

		$this->registry->db->query_write("
			UPDATE " . TABLE_PREFIX . "attachment
			SET state = 'moderation'
			WHERE attachmentid IN (" . implode(", ", $this->lists['attachmentids']) . ")
		");

		$this->post_moderate('unapprove');

		return true;
	}

	/**
	* Any code to run after approving
	*
	*/
	function post_moderate($type = 'approve')
	{
		foreach ($this->lists['content'] AS $contenttypeid => $list)
		{
			if (!($attach =& vB_Attachment_Dm_Library::fetch_library($this->registry, $contenttypeid)))
			{
				return false;
			}
			if ($type == 'approve')
			{
				$attach->post_approve($this);
			}
			else
			{
				$attach->post_unapprove($this);
			}
			unset($attach);
		}

		return true;
	}

	/**
	* Saves the data from the object into the specified database tables
	* Overwrites parent
	*
 	* @param	boolean	Do the query?
	* @param	mixed	Whether to run the query now; see db_update() for more info
	* @param bool 	Whether to return the number of affected rows.
	* @param bool		Perform REPLACE INTO instead of INSERT
	* @param bool		Perfrom INSERT IGNORE instead of INSERT
	*
	* @return	mixed	If this was an INSERT query, the INSERT ID is returned
	*/
	function save($doquery = true, $delayed = false, $affected_rows = false, $replace = false, $ignore = false)
	{
		if ($this->has_errors())
		{
			return false;
		}

		if (!$this->pre_save($doquery))
		{
			return false;
		}

		if ($this->condition === null)
		{
			$return = $this->db_insert(TABLE_PREFIX, $this->table, $doquery);
			// If no displayorder is set then default displayorder to be order of attachment insertion
			if (!$this->fetch_field('displayorder'))
			{
				$this->registry->db->query_write("
					UPDATE " . TABLE_PREFIX . "attachment SET displayorder = $return WHERE attachmentid = $return
				");
			}
			$this->set('attachmentid', $return);
		}
		else
		{
			$return = $this->db_update(TABLE_PREFIX, $this->table, $this->condition, $doquery, $delayed, $affected_rows);
		}

		if ($return AND $this->post_save_each($doquery) AND $this->post_save_once($doquery))
		{
			return $return;
		}
		else
		{
			return false;
		}
	}

	/**
	* Additional data to update after a save call (such as denormalized values in other tables).
	* In batch updates, is executed for each record updated.
	*
	* @param	boolean	Do the query?
	*/
	function post_save_each($doquery = true)
	{
		if ($filedataid = intval($this->attachment['filedataid']) AND $this->condition === null)
		{
			// Update the refcount in the filedata table
			$this->registry->db->query_write("
				UPDATE " . TABLE_PREFIX . "filedata AS fd
				SET fd.refcount = (
					SELECT COUNT(*)
					FROM " . TABLE_PREFIX . "attachment AS a
					WHERE fd.filedataid = a.filedataid
					GROUP BY a.filedataid
				)
				WHERE fd.filedataid = $filedataid
			");
		}

		return parent::post_save_each($doquery);
	}
}

/**
* Class to do data save/delete operations for just the Filedata table
*
* @package	vBulletin
* @version	$Revision: 35688 $
* @date		$Date: 2010-03-04 15:40:16 -0800 (Thu, 04 Mar 2010) $
*/
class vB_DataManager_Filedata extends vB_DataManager_AttachData
{
	/**
	* Array of recognized and required fields for attachment inserts
	*
	* @var	array
	*/
	var $validfields = array(
		'filedataid'         => array(TYPE_UINT,     REQ_INCR),
		'userid'             => array(TYPE_UINT,     REQ_YES),
		'dateline'           => array(TYPE_UNIXTIME, REQ_AUTO),
		'filedata'           => array(TYPE_BINARY,   REQ_NO,   VF_METHOD),
		'filesize'           => array(TYPE_UINT,     REQ_YES),
		'filehash'           => array(TYPE_STR,      REQ_YES,  VF_METHOD, 'verify_md5'),
		'thumbnail'          => array(TYPE_BINARY,   REQ_NO,   VF_METHOD),
		'thumbnail_dateline' => array(TYPE_UNIXTIME, REQ_AUTO),
		'thumbnail_filesize' => array(TYPE_UINT,     REQ_NO),
		'extension'          => array(TYPE_STR,      REQ_YES),
		'refcount'           => array(TYPE_UINT,     REQ_NO),
		'width'              => array(TYPE_UINT,     REQ_NO),
		'height'             => array(TYPE_UINT,     REQ_NO),
		'thumbnail_width'    => array(TYPE_UINT,     REQ_NO),
		'thumbnail_height'   => array(TYPE_UINT,     REQ_NO),
	);

	/**
	* The main table this class deals with
	*
	* @var	string
	*/
	var $table = 'filedata';

	/**
	* Condition template for update query
	* This is for use with sprintf(). First key is the where clause, further keys are the field names of the data to be used.
	*
	* @var	array
	*/
	var $condition_construct = array('filedataid = %1$d', 'filedataid');

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	public function __construct(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::__construct($registry, $errtype);

		($hook = vBulletinHook::fetch_hook('attachdata_start')) ? eval($hook) : false;
	}

	public function pre_delete($doquery = true)
	{
		return parent::pre_delete('filedata', $doquery);
	}

	public function delete($doquery = true)
	{
		if (!$this->pre_delete($doquery) OR empty($this->lists['filedataids']))
		{
			return $false;
		}

		if (!empty($this->lists['filedataids']))
		{
			$this->registry->db->query_write("
				DELETE FROM " . TABLE_PREFIX . "filedata
				WHERE filedataid IN (" . implode(", ", array_keys($this->lists['filedataids'])) . ")
			");

			$this->registry->db->query_write("
				DELETE FROM " . TABLE_PREFIX . "attachmentcategoryuser
				WHERE filedataid IN (" . implode(", ", array_keys($this->lists['filedataids'])) . ")
			");

			if ($this->storage == 'fs')
			{
				require_once(DIR . '/includes/functions_file.php');
				foreach ($this->lists['filedataids'] AS $filedataid => $userid)
				{
					@unlink(fetch_attachment_path($userid, $filedataid));
					@unlink(fetch_attachment_path($userid, $filedataid, true));
				}
			}

			// unset filedataids so that the post_delete function doesn't bother calculating refcount for the records that we just removed
			unset($this->lists['filedataids']);
		}

		if (!empty($this->lists['attachmentids']))
		{
			$this->registry->db->query_write("
				DELETE FROM " . TABLE_PREFIX . "attachment
				WHERE attachmentid IN (" . implode(", ", $this->lists['attachmentids']) . ")
			");
		}

		$this->post_delete();

		return true;
	}

	/**
	* Saves the data from the object into the specified database tables
	* Overwrites parent
	*
	* @return	mixed	If this was an INSERT query, the INSERT ID is returned
	*/
	function save($doquery = true, $delayed = false)
	{
		if ($this->has_errors())
		{
			return false;
		}

		if (!$this->pre_save($doquery))
		{
			return false;
		}

		if ($filedataid = $this->pre_save_filedata($doquery))
		{
			if (!$filedataid)
			{
				return false;
			}

			if ($filedataid !== true)
			{
				// this is an insert and file already exists
				$this->attachment['filedataid'] = $this->filedata['filedataid'] = $filedataid;

				$this->post_save_each($doquery);
				$this->post_save_once($doquery);

				// this is an insert and file already exists
				return $filedataid;
			}
		}

		if ($this->condition === null)
		{
			$return = $this->db_insert(TABLE_PREFIX, $this->table, $doquery);
			$this->set('filedataid', $return);
		}
		else
		{
			$return = $this->db_update(TABLE_PREFIX, $this->table, $this->condition, $doquery, $delayed);
		}

		if ($return AND $this->post_save_each($doquery) AND $this->post_save_once($doquery) AND $this->post_save_each_filedata($doquery))
		{
			return $return;
		}
		else
		{
			return false;
		}
	}
}

/**
* Class to do data save/delete operations for Attachment/Filedata table
*
* @package	vBulletin
* @version	$Revision: 35688 $
* @date		$Date: 2010-03-04 15:40:16 -0800 (Thu, 04 Mar 2010) $
*/
class vB_DataManager_AttachmentFiledata extends vB_DataManager_Attachment
{
	var $validfields = array(
		//attachment fields
		'attachmentid'       => array(TYPE_UINT,       REQ_INCR),
		'state'              => array(TYPE_STR,        REQ_NO),
		'counter'            => array(TYPE_UINT,       REQ_NO),
		'posthash'           => array(TYPE_STR,        REQ_NO,   VF_METHOD, 'verify_md5_alt'),
		'contenttypeid'      => array(TYPE_UINT,       REQ_YES),
		'contentid'          => array(TYPE_UINT,       REQ_NO),
		'caption'            => array(TYPE_NOHTMLCOND, REQ_NO),
		'reportthreadid'     => array(TYPE_UINT,       REQ_NO),
		'displayorder'       => array(TYPE_UINT,       REQ_NO),

		// Shared fields
		'userid'             => array(TYPE_UINT,       REQ_YES),
		'dateline'           => array(TYPE_UNIXTIME,   REQ_AUTO),
		'filename'           => array(TYPE_STR,        REQ_YES,  VF_METHOD, 'verify_filename'),

		// filedata fields
		'filedata'           => array(TYPE_BINARY,     REQ_NO,   VF_METHOD),
		'filesize'           => array(TYPE_UINT,       REQ_YES),
		'filehash'           => array(TYPE_STR,        REQ_YES,  VF_METHOD, 'verify_md5'),
		'thumbnail'          => array(TYPE_BINARY,     REQ_NO,   VF_METHOD),
		'thumbnail_dateline' => array(TYPE_UNIXTIME,   REQ_AUTO),
		'thumbnail_filesize' => array(TYPE_UINT,       REQ_NO),
		'extension'          => array(TYPE_STR,        REQ_YES),
		'refcount'           => array(TYPE_UINT,       REQ_NO),
		'width'              => array(TYPE_UINT,       REQ_NO),
		'height'             => array(TYPE_UINT,       REQ_NO),
		'thumbnail_width'    => array(TYPE_UINT,       REQ_NO),
		'thumbnail_height'   => array(TYPE_UINT,       REQ_NO),
	);

	/**
	* Constructor - checks that the registry object has been passed correctly.
	*
	* @param	vB_Registry	Instance of the vBulletin data registry object - expected to have the database object as one of its $this->db member.
	* @param	integer		One of the ERRTYPE_x constants
	*/
	public function __construct(&$registry, $errtype = ERRTYPE_STANDARD)
	{
		parent::__construct($registry, $errtype);

		($hook = vBulletinHook::fetch_hook('attachdata_start')) ? eval($hook) : false;
	}

	/**
	* Takes valid data and sets it as part of the data to be saved
	*
	* @param	string	The name of the field to which the supplied data should be applied
	* @param	mixed	The data itself
	*/
	function do_set($fieldname, &$value)
	{
		$this->setfields["$fieldname"] = true;

		$tables = array();

		switch ($fieldname)
		{
			case 'userid':
			case 'dateline' :
			{
				$tables = array('attachment', 'filedata');
			}
			break;

			case 'filedata':
			case 'filesize':
			case 'filehash':
			case 'thumbnail':
			case 'thumbnail_dateline':
			case 'thumbnail_filesize':
			case 'extension':
			case 'refcount':
			case 'width':
			case 'height':
			case 'thumbnail_width':
			case 'thumbnail_height':
			{
				$tables = array('filedata');
			}
			break;

			default:
			{
				$tables = array('attachment');
			}
		}

		($hook = vBulletinHook::fetch_hook('attachdata_doset')) ? eval($hook) : false;

		foreach ($tables AS $table)
		{
			$this->{$table}["$fieldname"] =& $value;
		}
	}

	/**
	* Saves attachment to the database
	*
	* @return	mixed
	*/
	public function save($doquery = true)
	{
		if ($this->has_errors())
		{
			return false;
		}

		if (!$this->pre_save($$doquery))
		{
			return 0;
		}

		$insertfiledata = true;
		if ($filedataid = $this->pre_save_filedata($doquery))
		{
			if (!$filedataid)
			{
				return false;
			}

			if ($filedataid !== true)
			{
				// this is an insert and file already exists
				// Check if file is already attached to this content
				if ($info = $this->registry->db->query_first(
					($this->info['contentid'] ? "(" : "") .
						"SELECT filedataid, attachmentid
						FROM " . TABLE_PREFIX . "attachment
						WHERE
							filedataid = " . intval($filedataid) . "
								AND
							posthash = '" . $this->registry->db->escape_string($this->fetch_field('posthash')) . "'
								AND
							contentid = 0
					" . ($this->info['contentid'] ? ") UNION (" : "") . "
					" . ($this->info['contentid'] ? "
						SELECT filedataid, attachmentid
						FROM " . TABLE_PREFIX . "attachment
						WHERE
							filedataid = " . intval($filedataid) . "
								AND
							contentid = " . intval($this->info['contentid']) . "
								AND
							contenttypeid = " . intval($this->fetch_field('contenttypeid')) . "
					)
						" : "") . "
				"))
				{
					// really just do nothing since this file is already attached to this content
					return $info['attachmentid'];
				}

				$this->attachment['filedataid'] = $filedataid;
				$insertfiledata = false;
				unset($this->validfields['filedata'], $this->validfields['filesize'],
					$this->validfields['filehash'], $this->validfields['thumbnail'], $this->validfields['thumbnail_dateline'],
					$this->validfields['thumbnail_filesize'], $this->validfields['extension'], $this->validfields['refcount']
				);
			}
		}

		if ($this->condition)
		{
			$return = $this->db_update(TABLE_PREFIX, 'thread', $this->condition, $doquery);
			if ($return)
			{
				$this->db_update(TABLE_PREFIX, 'filedataid', 'filedataid = ' . $this->fetch_field('filedataid'), $doquery);
			}
		}
		else
		{
			// insert query
			$return = $this->attachment['attachmentid'] = $this->db_insert(TABLE_PREFIX, 'attachment', $doquery);

			if ($return)
			{
				$this->do_set('attachmentid', $return);

				if ($insertfiledata)
				{
					$filedataid = $this->filedata['filedataid'] = $this->attachment['filedataid'] = $this->db_insert(TABLE_PREFIX, 'filedata', $doquery);
				}
				if ($doquery)
				{
					$this->dbobject->query_write("UPDATE " . TABLE_PREFIX . "attachment SET filedataid = $filedataid WHERE attachmentid = $return");
					// Verify categoryid
					if (!intval($this->info['categoryid']) OR $this->dbobject->query_first("
						SELECT categoryid
						FROM " . TABLE_PREFIX . "attachmentcategory
						WHERE
							userid = " . $this->fetch_field('userid') . "
								AND
							categoryid = " . intval($this->info['categoryid']) . "
					"))
					{
						$this->dbobject->query_write("
							INSERT IGNORE INTO " . TABLE_PREFIX . "attachmentcategoryuser
								(userid, filedataid, categoryid, filename, dateline)
							VALUES
								(" . $this->fetch_field('userid') . ", $filedataid, " . intval($this->info['categoryid']) . ", '" . $this->dbobject->escape_string($this->fetch_field('filename')) . "', " . TIMENOW . ")
						");
					}
				}
			}
		}

		if ($return)
		{
			$this->post_save_each($doquery);
			$this->post_save_once($doquery);
			if ($insertfiledata)
			{
				$this->post_save_each_filedata($doquery);
			}
		}

		return $return;
	}
}

/*======================================================================*\
|| ####################################################################
|| # CVS: $RCSfile$ - $Revision: 35688 $
|| ####################################################################
\*======================================================================*/
?>