<?php
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 6.2.3 - Licence Number L3138846
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2026 MH Sub I, LLC dba vBulletin. 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   # ||
|| ###################################################################### ||
\*========================================================================*/

// ######################## SET PHP ENVIRONMENT ###########################

// ##################### DEFINE IMPORTANT CONSTANTS #######################
define('CVS_REVISION', '$RCSfile$ - $Revision: 122851 $');

// #################### PRE-CACHE TEMPLATES AND DATA ######################
global $phrasegroups, $specialtemplates, $vbphrase, $vbulletin;
$phrasegroups = ['hooks'];
$specialtemplates = [];

// ########################## REQUIRE BACK-END ############################
require_once(dirname(__FILE__) . '/global.php');
require_once(DIR . '/includes/adminfunctions_product.php');
// needed for build_all_styles and perhaps other functions
require_once(DIR . '/includes/adminfunctions_template.php');

$assertor = vB::getDbAssertor();
$prod_lib = vB_Library::instance('product');

// ######################## CHECK ADMIN PERMISSIONS #######################
// don't allow demo version or admin with no permission to administer products
if (is_demo_mode() OR !can_administer('canadminproducts'))
{
	print_cp_no_permission();
}

$vbulletin->input->clean_array_gpc('r', [
	'productid' => vB_Cleaner::TYPE_STR
]);

// ############################# LOG ACTION ###############################
log_admin_action(($vbulletin->GPC['productid'] != 0 ? 'Product id = ' . $vbulletin->GPC['productid'] : ''));

// #############################################################################
// ########################### START MAIN SCRIPT ###############################
// #############################################################################

if ($_REQUEST['do'] != 'download' AND $_REQUEST['do'] != 'productexport')
{
	$extraheaders = [
		get_admincp_script_tag('vbulletin_product.js'),
	];

	if (in_array($_REQUEST['do'], ['productadd', 'productedit']))
	{
		$extraheaders = [
			get_admincp_css_tag('codemirror/lib/codemirror.css'),
			get_admincp_css_tag('codemirror/addon/hint/show-hint.css'),
		];
	}

	print_cp_header($vbphrase['hook_products_system'], $extraheaders);
}

if (empty($_REQUEST['do']))
{
	$_REQUEST['do'] = 'product';
}

if (in_array($_REQUEST['do'], ['product', 'productadd', 'productedit', 'extensions']))
{
	if (!$vbulletin->options['enablehooks'] OR defined('DISABLE_HOOKS'))
	{
		$hooksEnabled = false;

		if (!$vbulletin->options['enablehooks'])
		{
			print_warning_table($vbphrase['hooks_disabled_options']);
		}
		else
		{
			print_warning_table($vbphrase['hooks_disable_config']);
		}
	}
	else
	{
		$hooksEnabled = true;
	}
}

// #############################################################################
// ####################          Products                   ####################
// #############################################################################

if ($_REQUEST['do'] == 'product')
{
	print_form_header2('', '');
	print_table_start2(['last-column-jump']);

	$headers = [
		$vbphrase['title'],
		$vbphrase['version_products'],
		$vbphrase['description_gcpglobal'],
		$vbphrase['controls'],
	];
	$colcount = count($headers);

	// left align except for the last column which is right aligned.
	$colalign = array_fill(0, $colcount -1, 'vbleft');
	$colalign[] = 'vbright';

	print_table_header($vbphrase['installed_products'], $colcount);
	print_cells_row2($headers, 'thead', $colalign);

	print_cells_row2(['<strong>vBulletin</strong>', $vbulletin->options['templateversion'], '', ''], '', $colalign);

	$products = $assertor->getRows('product', [], 'title');
	// Also grab options associated with each product
	$productIds = [];
	foreach ($products AS $product)
	{
		$productIds[$product['productid']] = $product['productid'];
	}
	// 'vbulletin' is not stored in the product table ATM, but just in case the product model changes in the future,
	// pre-emptively unset the default product as that'll return too many options and not be useful.
	unset($productIds['vbulletin']);
	$productOptions = [];
	$productOptionsRows = $assertor->getRows('setting', ['product' => $productIds], 'product');
	foreach ($productOptionsRows AS $__row)
	{
		$__product = $__row['product'];
		$__varname = $__row['varname'];
		if (!isset($productOptions[$__product]))
		{
			$productOptions[$__product] = [];
		}
		$productOptions[$__product][$__varname] = $__row;
	}

	foreach ($products AS $product)
	{
		$__pid = $product['productid'];
		$title = htmlspecialchars_uni($product['title']);
		if (!$product['active'])
		{
			$title = "<strike>$title</strike>";
		}

		if ($product['url'])
		{
			$title = '<a href="' . htmlspecialchars_uni($product['url']) . "\" target=\"_blank\">$title</a>";
		}

		$options = [
			'productedit' => $vbphrase['edit']
		];

		if ($product['versioncheckurl'])
		{
			$options['productversioncheck'] = $vbphrase['check_version'];
		}

		if ($product['active'])
		{
			$options['productdisable'] = $vbphrase['disable'];
		}
		else
		{
			$options['productenable'] = $vbphrase['enable'];
		}

		$options['productexport'] = $vbphrase['export'];
		$options['productdelete'] = $vbphrase['uninstall'];

		// pretty sure this is obsolete.
		$safeid = preg_replace('#[^a-z0-9_]#', '', $__pid);
		if (file_exists(DIR . '/includes/version_' . $safeid . '.php'))
		{
			include_once(DIR . '/includes/version_' . $safeid . '.php');
		}
		$define_name = 'FILE_VERSION_' . strtoupper($safeid);
		if (defined($define_name) AND constant($define_name) !== '')
		{
			$product['version'] = constant($define_name);
		}

		$showOptMax = 2;
		$__productOptionsText = "";
		$__productOptionsShowMoreArray = [];
		if (!empty($productOptions[$__pid]))
		{
			$__optCount = 0;
			foreach ($productOptions[$__pid] AS $__opt)
			{
				$__optCount++;

				$settingli = '<li class="product-options__list-item">' . get_setting_link($__opt) . "</li>\n";
				if ($showOptMax >= $__optCount)
				{
					$__productOptionsText .= $settingli;
				}
				else
				{
					$__productOptionsShowMoreArray[] = $settingli;
				}
			}
		}
		if ($__productOptionsShowMoreArray)
		{
			$__moreOptCount = count($__productOptionsShowMoreArray);
			if ($__moreOptCount > 1)
			{
				$collapseId = uniqid("acp-collapse-");
				$controls = get_collapse_controls(
					sprintf($vbphrase['show_x_more_options'], $__moreOptCount),
					$vbphrase['hide_additional_options'],
					$collapseId
				);

				$__productOptionsText .=
					open_collapse_group($collapseId, 'div', true, ['collapse'], true) . "\n"
					. $controls . "\n"
					. '<div class="collapse-content">'. implode('', $__productOptionsShowMoreArray) . "\n</div>"
					. close_collapse_group($collapseId, 'div', true);
			}
			else
			{
				$__productOptionsText .= $__productOptionsShowMoreArray[0];
			}
		}
		if (!empty($__productOptionsText))
		{
			$__productOptionsText = "
				<div class=\"product-options\">
					<div class=\"product-options__label\">{$vbphrase['related_options']}:</div>
					<ul class=\"product-options__list\">
						{$__productOptionsText}
					</ul>
				</div>
			";
		}

		$cells = [
			$title,
			htmlspecialchars_uni($product['version']),
			htmlspecialchars_uni($product['description']) . $__productOptionsText,
			construct_jump_control('prod', $product['productid'], $vbphrase['go'], $options),
		];

		print_cells_row2($cells, '', $colalign);
	}

	print_table_footer();
	echo '<p align="center">' . construct_link_code($vbphrase['add_import_product'], "product.php?do=productadd") . '</p>';
}

// #############################################################################

if ($_REQUEST['do'] == 'productversioncheck')
{
	$product = $assertor->getRow('product', ['productid' => $vbulletin->GPC['productid']]);

	if (!$product OR empty($product['versioncheckurl']))
	{
		print_stop_message2('invalid_product_specified');
	}

	$version_url = @vB_String::parseUrl($product['versioncheckurl']);
	if (!$version_url)
	{
		print_stop_message2('invalid_version_check_url_specified');
	}

	if (!$version_url['port'])
	{
		$version_url['port'] = 80;
	}
	if (!$version_url['path'])
	{
		$version_url['path'] = '/';
	}

	$fp = @fsockopen($version_url['host'], ($version_url['port'] ? $version_url['port'] : 80), $errno, $errstr, 10);
	if (!$fp)
	{
		print_stop_message2([
			'version_check_connect_failed_host_x_error_y',
			htmlspecialchars_uni($version_url['host']),
			htmlspecialchars_uni($errstr)
		]);
	}

	$send_headers = "POST $version_url[path] HTTP/1.0\r\n";
	$send_headers .= "Host: $version_url[host]\r\n";
	$send_headers .= "User-Agent: vBulletin Product Version Check\r\n";
	if ($version_url['query'])
	{
		$send_headers .= "Content-Type: application/x-www-form-urlencoded\r\n";
	}
	$send_headers .= "Content-Length: " . strlen($version_url['query']) . "\r\n";
	$send_headers .= "\r\n";

	fwrite($fp, $send_headers . $version_url['query']);

	$full_result = '';
	while (!feof($fp))
	{
		$result = fgets($fp, 1024);
		$full_result .= $result;
	}

	fclose($fp);

	preg_match('#^(.*)\r\n\r\n(.*)$#sU', $full_result, $matches);
	$headers = trim($matches[1]);
	$body = trim($matches[2]);

	if (preg_match('#<version productid="' . preg_quote($product['productid'], '#') . '">(.+)</version>#iU', $body, $matches))
	{
		$latest_version = $matches[1];
	}
	else if (preg_match('#<version>(.+)</version>#iU', $body, $matches))
	{
		$latest_version = $matches[1];
	}
	else
	{
		print_stop_message2('version_check_failed_not_found');
	}

	// see if we have a patch or something
	$safeid = preg_replace('#[^a-z0-9_]#', '', $product['productid']);
	if (file_exists(DIR . '/includes/version_' . $safeid . '.php'))
	{
		include_once(DIR . '/includes/version_' . $safeid . '.php');
	}
	$define_name = 'FILE_VERSION_' . strtoupper($safeid);
	if (defined($define_name) AND constant($define_name) !== '')
	{
		$product['version'] = constant($define_name);
	}

	print_form_header2('admincp/', '');
	print_table_start2();

	if (is_newer_version($latest_version, $product['version']))
	{
		print_table_header(construct_phrase($vbphrase['product_x_out_of_date'], htmlspecialchars_uni($product['title'])));
		print_label_row($vbphrase['installed_version'], htmlspecialchars_uni($product['version']));
		print_label_row($vbphrase['latest_version'], htmlspecialchars_uni($latest_version));
		if ($product['url'])
		{
			print_description_row(
				'<a href="admincp/' . htmlspecialchars_uni($product['url']) . '" target="_blank">' . $vbphrase['click_here_for_more_info'] . '</a>',
				false,
				2,
				'',
				'center'
			);
		}
	}
	else
	{
		print_table_header(construct_phrase($vbphrase['product_x_up_to_date'], htmlspecialchars_uni($product['title'])));
		print_label_row($vbphrase['installed_version'], htmlspecialchars_uni($product['version']));
		print_label_row($vbphrase['latest_version'], htmlspecialchars_uni($latest_version));
	}

	print_table_footer();
}

// #############################################################################

if ($_REQUEST['do'] == 'productdisable' OR $_REQUEST['do'] == 'productenable')
{
	$isDisable = ($_REQUEST['do'] == 'productdisable');

	$vbulletin->input->clean_array_gpc('r', [
		'confirmswitch' => vB_Cleaner::TYPE_BOOL
	]);

	$product = $assertor->getRow('product', ['productid' => $vbulletin->GPC['productid']]);
	if (!$product)
	{
		print_stop_message('invalid_product_specified');
	}

	$product_list = fetch_product_list(true);

	if ($isDisable)
	{
		// disabling a product -- disable all children
		$indexKey = 'parentproductid';
		$valueKey = 'productid';
		$phrase_name = 'additional_products_disable_x_y';
		$dependencyStatus = 1;
	}
  else
	{
		// enabling a product -- enable all parents
		$indexKey = 'productid';
		$valueKey = 'parentproductid';
		$phrase_name = 'additional_products_enable_x_y';
		$dependencyStatus = 0;
	}

	$result = $assertor->select('productdependency', [
		'dependencytype' => 'product',
		['field' => 'parentproductid', 'value' => '', vB_dB_Query::OPERATOR_KEY => vB_dB_Query::OPERATOR_NE]
	]);

	// If we are disabling we want a map of all parentproducts => childproducts
	// If we are enabling we want a map of childproducts => parentproducts
	// This is used by fetch_product_dependencies to generate either the list of all parents or all children
	// based on that.
	$dependencyList = vB_Utility_Functions::iterableColumnGrouped($result, $valueKey, $indexKey);
	$currentDependencies = fetch_product_dependencies($product['productid'], $dependencyList);

	$need_switch = [];
	foreach ($currentDependencies AS $dependencyid)
	{
		$dependency = $product_list[$dependencyid] ?? null;
		if ($dependency AND $dependency['active'] == $dependencyStatus)
		{
			// product exists and is enabled -- needs to be disabled
			$need_switch[$dependencyid] = $dependency['title'];
		}
	}

	if (!$vbulletin->GPC['confirmswitch'] AND count($need_switch) > 0)
	{
		// to do this, we need to update the status of some additional products,
		// so make sure the user knows what's going on

		$url = get_admincp_url('product', [
			'do' => $_REQUEST['do'],
			'productid' => $product['productid'],
			'confirmswitch' => 1,
		]);

		print_stop_message([
			$phrase_name,
			htmlspecialchars_uni($product['title']),
			'<li>' . implode('</li><li>', $need_switch) . '</li>',
			htmlspecialchars_uni($url),
		]);
	}

	// $need_switch is already escaped
	$product_update = array_keys($need_switch);
	$product_update[] = $vbulletin->GPC['productid'];

	// Do the product table
	$assertor->assertQuery('product', [
		vB_dB_Query::TYPE_KEY => vB_dB_Query::QUERY_UPDATE,
		'productid' => $product_update,
		'active' => ($isDisable ? 0 : 1),
	]);

	vB_Library::instance('product')->doProductChangeRebuilds();

	$redirecturl = get_admincp_url('product', ['do' => 'product']);
	print_stop_message($isDisable ? 'product_disabled_successfully' : 'product_enabled_successfully', $redirecturl);
}

// #############################################################################

if ($_REQUEST['do'] == 'productadd' OR $_REQUEST['do'] == 'productedit')
{
	if ($vbulletin->GPC['productid'])
	{
		$product = $assertor->getRow('product', ['productid' => $vbulletin->GPC['productid']]);
		$haveproduct = true;
	}
	else
	{
		$product = [
			'productid' => 0,
			'title' => '',
			'version' => '',
			'description' => '',
			'url' => '',
			'versioncheckurl' => '',
		];
		$haveproduct = false;
	}

	if (!$haveproduct)
	{
		print_form_header2('admincp/product', 'productimport', [], ['name' => 'uploadform', 'enctype' => 'multipart/form-data']);
		print_table_start2();
		print_table_header($vbphrase['import_product']);
		print_upload_row($vbphrase['upload_xml_file'], 'productfile', 999999999);
		print_input_row($vbphrase['import_xml_file'], 'serverfile', './includes/xml/product.xml');
		print_yes_no_row($vbphrase['allow_overwrite_upgrade_product'], 'allowoverwrite', 0);
		print_table_default_footer($vbphrase['import']);
	}

	print_form_header2('admincp/product', 'productsave');
	print_table_start2();

	if ($haveproduct)
	{
		print_table_header(construct_phrase($vbphrase['edit_product_x'], $product['productid']));
		print_label_row($vbphrase['product_id'], $product['productid']);

		construct_hidden_code('productid', $product['productid']);
		construct_hidden_code('editing', 1);
	}
	else
	{
		print_table_header($vbphrase['add_new_product']);
		print_input_row($vbphrase['product_id'], 'productid', '', true, 50, 25); // max length = 25
	}

	print_input_row($vbphrase['title'], 'title', $product['title'], true, 50, 50);
	print_input_row($vbphrase['version_products'], 'version', $product['version'], true, 50, 25);
	print_input_row($vbphrase['description_gcpglobal'], 'description', $product['description'], true, 50, 250);
	print_input_row($vbphrase['product_url'], 'url', $product['url'], true, 50, 250);
	print_input_row($vbphrase['version_check_url'], 'versioncheckurl', $product['versioncheckurl'], true, 50, 250);

	print_submit_row();

	// if we're editing a product, show the install/uninstall code options
	if ($haveproduct)
	{
		echo '<hr />';

		print_form_header2('admincp/product', 'productdependency');
		print_table_start2();
		construct_hidden_code('productid', $vbulletin->GPC['productid']);

		// the <label> tags in the product type are for 3.6 bug 349
		$dependency_types = [
			'php'       => $vbphrase['php_version'],
			'mysql'     => $vbphrase['mysql_version_products'],
			'vbulletin' => $vbphrase['vbulletin_version_products'],
			'product'   => $vbphrase['product_id'] . '</label>&nbsp;<input type="text" class="bginput" name="parentproductid" id="it_parentproductid" value="" size="15" maxlength="25" tabindex="1" /><label>',
		];

		$product_dependencies = $assertor->getRows('productdependency', ['productid' => $vbulletin->GPC['productid']], ['dependencytype', 'parentproductid', 'minversion']);
		if (count($product_dependencies))
		{
			print_table_header($vbphrase['existing_product_dependencies'], 4);
			print_cells_row([
				$vbphrase['dependency_type'],
				$vbphrase['compatibility_starts'],
				$vbphrase['incompatible_with'],
				$vbphrase['delete']
			], 1);

			foreach ($product_dependencies AS $product_dependency)
			{
				if ($product_dependency['dependencytype'] != 'product')
				{
					$dep_type = $dependency_types["$product_dependency[dependencytype]"];
				}
				else
				{
					$dep_type = $vbphrase['product'] . ' - ' . htmlspecialchars_uni($product_dependency['parentproductid']);
				}

				$depid = $product_dependency['productdependencyid'];

				print_cells_row([
					$dep_type,
					"<input type=\"text\" name=\"productdependency[$depid][minversion]\" value=\"" . htmlspecialchars_uni($product_dependency['minversion']) . "\" size=\"25\" maxlength=\"50\" tabindex=\"1\" />",
					"<input type=\"text\" name=\"productdependency[$depid][maxversion]\" value=\"" . htmlspecialchars_uni($product_dependency['maxversion']) . "\" size=\"25\" maxlength=\"50\" tabindex=\"1\" />",
					"<input type=\"checkbox\" name=\"productdependency[$depid][delete]\" value=\"1\" />"
				]);
			}

			print_table_break();
		}

		print_table_header($vbphrase['add_new_product_dependency']);
		print_radio_row($vbphrase['dependency_type'], 'dependencytype', $dependency_types);
		print_input_row($vbphrase['compatibility_starts_with_version'], 'minversion', '', true, 25, 50);
		print_input_row($vbphrase['incompatible_with_version_and_newer'], 'maxversion', '', true, 25, 50);

		print_submit_row();

		// #############################################
		echo '<hr />';

		print_form_header2('admincp/product', 'productcode');
		print_table_start2();
		construct_hidden_code('productid', $vbulletin->GPC['productid']);

		$productcodes = $assertor->getRows('productcode', ['productid' => $vbulletin->GPC['productid']], 'version');

		if (count($productcodes))
		{
			print_table_header($vbphrase['existing_install_uninstall_code'], 4);
			print_cells_row([
				$vbphrase['version_products'],
				$vbphrase['install_code'],
				$vbphrase['uninstall_code'],
				$vbphrase['delete']
			], 1);

			$productcodes_grouped = [];
			$productcodes_versions = [];

			foreach ($productcodes AS $productcode)
			{
				// have to be careful here, as version numbers are not necessarily unique
				$productcodes_versions["$productcode[version]"] = 1;
				$productcodes_grouped["$productcode[version]"][] = $productcode;
			}

			$productcodes_versions = array_keys($productcodes_versions);
			usort($productcodes_versions, 'version_sort');

			// It's weird that we don't treat these text boxes the same as the new entries, but there is a styling and real estate issue
			// because we display these much smaller and side by side and we either need to make CodeMirror do that or make these boxes
			// bigger and stacked.  Either way this is not a pressing problem to solve.
			$attributes = [
				// see note below.
				//			'dir' => 'ltr',
				'rows' => 4,
				'cols' => 40,
				'class' => 'textarea-full-width',
			];

			foreach ($productcodes_versions AS $version)
			{
				foreach ($productcodes_grouped["$version"] AS $productcode)
				{
					$basename = 'productcode[' . $productcode['productcodeid'] . ']';
					$cells = [
						construct_text_input($basename . '[version]', htmlspecialchars_uni($productcode['version']), ['size' => 10, 'style' => 'width:100%']),
						'<textarea ' . construct_control_attributes($basename . '[installcode]', $attributes) . '>' .  htmlspecialchars_uni($productcode['installcode']) . '</textarea>',
						'<textarea ' . construct_control_attributes($basename . '[uninstallcode]', $attributes) . '>' .  htmlspecialchars_uni($productcode['uninstallcode']) . '</textarea>',
						construct_input('checkbox', $basename . '[delete]', ['value' => 1]),
					];

					print_cells_row($cells);
				}
			}

			print_table_break();
		}

		print_table_header($vbphrase['add_new_install_uninstall_code']);

		print_input_row($vbphrase['version_products'], 'version');

		$attributes = [
			'rows' => 22,
			// We set this for the templates because the code isn't really in the "native" language displayed and it's
			// not clear that we ever want it in rtl.  The same logic applies here but we've never done it and so
			// not going to make the change at present.
			//			'dir' => 'ltr',
			'data-mode' => 'application/x-httpd-php-open',
		];
		$classes = ['js-code-mirror', 'code', 'textarea-full-width'];

		print_textarea_row2($vbphrase['install_code'], 'installcode', '', $attributes, $classes);
		print_textarea_row2($vbphrase['uninstall_code'], 'uninstallcode', '', $attributes, $classes);

		activateCodeMirrorPHP();
		print_submit_row();
	}
}

// #############################################################################

if ($_POST['do'] == 'productsave')
{
	// Check to see if it is a duplicate.
	$vbulletin->input->clean_array_gpc('p', [
		'editing'         => vB_Cleaner::TYPE_BOOL,
		'title'           => vB_Cleaner::TYPE_STR,
		'version'         => vB_Cleaner::TYPE_STR,
		'description'     => vB_Cleaner::TYPE_STR,
		'url'             => vB_Cleaner::TYPE_STR,
		'versioncheckurl' => vB_Cleaner::TYPE_STR,
		'confirm'         => vB_Cleaner::TYPE_BOOL,
	]);

	if ($vbulletin->GPC['url'] AND !preg_match('#^[a-z0-9]+:#i', $vbulletin->GPC['url']))
	{
		$vbulletin->GPC['url'] = 'http://' . $vbulletin->GPC['url'];
	}
	if ($vbulletin->GPC['versioncheckurl'] AND !preg_match('#^[a-z0-9]+:#i', $vbulletin->GPC['versioncheckurl']))
	{
		$vbulletin->GPC['versioncheckurl'] = 'http://' . $vbulletin->GPC['versioncheckurl'];
	}

	if (!$vbulletin->GPC['productid'] OR !$vbulletin->GPC['title'] OR !$vbulletin->GPC['version'])
	{
		print_stop_message2('please_complete_required_fields');
	}

	if (strtolower($vbulletin->GPC['productid']) == 'vbulletin')
	{
		print_stop_message2(['product_x_installed_version_y_z', 'vBulletin', $vbulletin->options['templateversion'], $vbulletin->GPC['version']]);
	}

	if (!$vbulletin->GPC['editing'] AND $existingprod = $assertor->getRow('product', ['productid' => $vbulletin->GPC['productid']]))
	{
		print_stop_message2(['product_x_installed_version_y_z', $vbulletin->GPC['title'], $existingprod['version'], $vbulletin->GPC['version']]);
	}

	$invalid_version_structure = [0, 0, 0, 0, 0, 0];
	if (fetch_version_array($vbulletin->GPC['version']) == $invalid_version_structure)
	{
		print_stop_message2('invalid_product_version');
	}

	if ($vbulletin->GPC['editing'])
	{
		$assertor->update('product', [
				'title' => $vbulletin->GPC['title'],
				'description' => $vbulletin->GPC['description'],
				'version' => $vbulletin->GPC['version'],
				'url' => $vbulletin->GPC['url'],
				'versioncheckurl' => $vbulletin->GPC['versioncheckurl'],
			],
			['productid' => $vbulletin->GPC['productid']]
		);
	}
	else
	{
		// product IDs must match #^[a-z0-9_]+$# and must be max 25 chars
		if (!preg_match('#^[a-z0-9_]+$#s', $vbulletin->GPC['productid']) OR strlen($vbulletin->GPC['productid']) > 25)
		{
			$sugg = preg_replace('#\s+#s', '_', strtolower($vbulletin->GPC['productid']));
			$sugg = preg_replace('#[^\w]#s', '', $sugg);
			$sugg = str_replace('__', '_', $sugg);
			$sugg = substr($sugg, 0, 25);
			print_stop_message2(['product_id_invalid', htmlspecialchars_uni($vbulletin->GPC['productid']), $sugg]);
		}

		// reserve 'vb' prefix for official vBulletin products
		if (!$vbulletin->GPC['confirm'] AND strtolower(substr($vbulletin->GPC['productid'], 0, 2)) == 'vb')
		{
			print_form_header2('admincp/product', 'productsave');
			print_table_start2();
			print_table_header($vbphrase['vbulletin_message']);
			print_description_row(
				htmlspecialchars_uni($vbulletin->GPC['title']) . ' ' . htmlspecialchars_uni($vbulletin->GPC['version']) .
				'<dfn>' . htmlspecialchars_uni($vbulletin->GPC['description']) . '</dfn>'
			);
			print_input_row($vbphrase['vb_prefix_reserved'], 'productid', $vbulletin->GPC['productid'], true, 35, 25);
			construct_hidden_code('title', $vbulletin->GPC['title']);
			construct_hidden_code('description', $vbulletin->GPC['description']);
			construct_hidden_code('version', $vbulletin->GPC['version']);
			construct_hidden_code('confirm', 1);
			construct_hidden_code('url', $vbulletin->GPC['url']);
			construct_hidden_code('versioncheckurl', $vbulletin->GPC['versioncheckurl']);
			print_submit_row();
			print_cp_footer();

			// execution terminates here
		}

		/* insert query */
		$assertor->insert('product', [
			'productid' => $vbulletin->GPC['productid'],
			'title' => $vbulletin->GPC['title'],
			'description' => $vbulletin->GPC['description'],
			'version' => $vbulletin->GPC['version'],
			'active' => 1,
			'url' => $vbulletin->GPC['url'],
			'versioncheckurl' => $vbulletin->GPC['versioncheckurl']
		]);
	}

	// update the products datastore
	$prod_lib->buildProductDatastore();

	if (defined('DEV_AUTOEXPORT') AND DEV_AUTOEXPORT)
	{
		require_once(DIR . '/includes/functions_filesystemxml.php');
		autoexport_write_product($vbulletin->GPC['productid']);
	}

	print_stop_message2(['product_x_updated', $vbulletin->GPC['productid']], 'product', ['do' => 'product']);
}

// #############################################################################

if ($_POST['do'] == 'productdependency')
{
	$vbulletin->input->clean_array_gpc('p', [
		'dependencytype'	=> vB_Cleaner::TYPE_STR,
		'parentproductid'	=> vB_Cleaner::TYPE_STR,
		'minversion'		=> vB_Cleaner::TYPE_STR,
		'maxversion'		=> vB_Cleaner::TYPE_STR,
		'productdependency'	=> vB_Cleaner::TYPE_ARRAY
	]);

	$product = $assertor->getRow('product', ['productid' => $vbulletin->GPC['productid']]);

	if (!$product)
	{
		print_stop_message2('invalid_product_specified');
	}

	if ($vbulletin->GPC['dependencytype'] != 'product')
	{
		$vbulletin->GPC['parentproductid'] = '';
	}

	if ($vbulletin->GPC['dependencytype'] OR $vbulletin->GPC['parentproductid'])
	{
		if ($vbulletin->GPC['minversion'] OR $vbulletin->GPC['maxversion'])
		{
			/* insert query */
			$assertor->insert('productdependency', [
				'productid' => $vbulletin->GPC['productid'],
				'dependencytype' => $vbulletin->GPC['dependencytype'],
				'parentproductid' => $vbulletin->GPC['parentproductid'],
				'minversion' => $vbulletin->GPC['minversion'],
				'maxversion' => $vbulletin->GPC['maxversion']
			]);
		}
		else
		{
			print_stop_message2('please_complete_required_fields');
		}
	}

	foreach ($vbulletin->GPC['productdependency'] AS $productdependencyid => $product_dependency)
	{
		$productdependencyid = intval($productdependencyid);

		if ($product_dependency['delete'])
		{
			$assertor->delete('productdependency', ['productdependencyid' => $productdependencyid]);
		}
		else
		{
			$assertor->update('productdependency',
				['minversion' => $product_dependency['minversion'], 'maxversion' => $product_dependency['maxversion']],
				['productdependencyid' => $productdependencyid]
			);
		}
	}

	if (defined('DEV_AUTOEXPORT') AND DEV_AUTOEXPORT)
	{
		require_once(DIR . '/includes/functions_filesystemxml.php');
		autoexport_write_product($vbulletin->GPC['productid']);
	}

	print_stop_message2(['product_x_updated',  $vbulletin->GPC['productid']], 'product', ['do' => 'productedit','productid'=>$vbulletin->GPC['productid']]);
}

// #############################################################################

if ($_POST['do'] == 'productcode')
{
	$vbulletin->input->clean_array_gpc('p', [
		'version'		=> vB_Cleaner::TYPE_STR,
		'installcode'	=> vB_Cleaner::TYPE_STR,
		'uninstallcode'	=> vB_Cleaner::TYPE_STR,
		'productcode'	=> vB_Cleaner::TYPE_ARRAY
	]);
	$product = $assertor->getRow('product', ['productid' => $vbulletin->GPC['productid']]);
	if (!$product)
	{
		print_stop_message2('invalid_product_specified');
	}

	if ($vbulletin->GPC['version'] AND ($vbulletin->GPC['installcode'] OR $vbulletin->GPC['uninstallcode']))
	{
		$assertor->insert('productcode', [
			'productid' => $vbulletin->GPC['productid'],
			'version' => $vbulletin->GPC['version'],
			'installcode' => $vbulletin->GPC['installcode'],
			'uninstallcode' => $vbulletin->GPC['uninstallcode']
		]);
	}

	foreach ($vbulletin->GPC['productcode'] AS $productcodeid => $productcode)
	{
		$productcodeid = intval($productcodeid);

		// this is a checkbox and won't be set if it isn't true.
		if ($productcode['delete'] ?? false)
		{
			$assertor->delete('productcode', ['productcodeid' => $productcodeid]);
		}
		else
		{
			$assertor->update('productcode',
				['version' => $productcode['version'], 'installcode' => $productcode['installcode'], 'uninstallcode' => $productcode['uninstallcode']],
				['productcodeid' => $productcodeid]
			);
		}
	}

	if (defined('DEV_AUTOEXPORT') AND DEV_AUTOEXPORT)
	{
		require_once(DIR . '/includes/functions_filesystemxml.php');
		autoexport_write_product($vbulletin->GPC['productid']);
	}

	print_stop_message2(['product_x_updated', $vbulletin->GPC['productid']], 'product', ['do' => 'productedit', 'productid' => $vbulletin->GPC['productid']]);
}

// #############################################################################

if ($_POST['do'] == 'productkill')
{
	// seems like we should do something with more clarity to the user but not poking it.
	if (strtolower($vbulletin->GPC['productid']) == 'vbulletin')
	{
		print_cp_redirect2('product', ['do' => 'product'], 1, 'admincp');
	}

	vB_Library::instance('product')->uninstallProduct($vbulletin->GPC['productid']);

	// The DISABLE_PRODUCT_REDIRECT can be set by the product install/uninstall steps to avoid the redirect
	// this is obsolete (it appears to have been used by the old vb3/4 blog package to show some kind of
	// message to the user that would not be legible if the redirect triggers immediately after showing it).
	//
	// Unless it's used by a 3rd party it's not used and we the idea of printing output on install/uninstall
	// is going to play merry havoc with handling this in other context (such as in the upgrade).  If we still
	// need it we should provide a more robust mechanism for product specific install/uninstall messages.
	$redirecturl = '';
	if (!defined('DISABLE_PRODUCT_REDIRECT'))
	{
		$redirecturl = get_admincp_url('product', ['do' => 'product']);
	}
	print_stop_message(['product_x_uninstalled', $vbulletin->GPC['productid']], $redirecturl);
}

// #############################################################################

if ($_REQUEST['do'] == 'productdelete')
{
	if (strtolower($vbulletin->GPC['productid']) == 'vbulletin')
	{
		print_cp_redirect2('product', ['do' => 'product'], 1, 'admincp');
	}

	$dependency_result = $assertor->getRows('productdependency', [
		vB_dB_Query::CONDITIONS_KEY => [
			['field' => 'dependencytype', 'value' => 'product', vB_dB_Query::OPERATOR_KEY => vB_dB_Query::OPERATOR_EQ],
			['field' => 'parentproductid', 'value' => '', vB_dB_Query::OPERATOR_KEY => vB_dB_Query::OPERATOR_NE]
		]
	]);

	// find child products -- these may break if we uninstall this
	$dependency_list = [];
	foreach ($dependency_result AS $dependency)
	{
		$dependency_list["$dependency[parentproductid]"][] = $dependency['productid'];
	}

	$children = fetch_product_dependencies($vbulletin->GPC['productid'], $dependency_list);

	$product_list = fetch_product_list(true);

	$children_text = [];
	foreach ($children AS $childproductid)
	{
		$childproduct = $product_list["$childproductid"];
		if ($childproduct)
		{
			$children_text[] = $childproduct['title'];
		}
	}

	if ($children_text)
	{
		$affected_children = construct_phrase(
			$vbphrase['uninstall_product_break_products_x'],
			'<li>' . implode('</li><li>', $children_text) . '</li>'
		);
	}
	else
	{
		$affected_children = '';
	}

	// find widgetinstances that will be deleted
	$widgetids = $assertor->getColumn('widget', 'widgetid', ['product' => $vbulletin->GPC['productid']]);
	$widget_info = "";
	if (!empty($widgetids))
	{
		/*
			Just a list of integer widgetinstanceids aren't gonna be very helpful to a non-developer.
			Per Wayne, the page name (Page Name field when you save a page in site builder) of the pages
			that contain the widgetinstances would be most helpful, so let's get those page names.
			Since pagetemplates hold the widgetinstances, and multiple pages might use the same pagetemplate,
			the list might get a bit long... hopefully not long enough to require pagination though.
		 */
		$pagetemplateids = $assertor->getColumn('widgetinstance', 'pagetemplateid', ['widgetid' => $widgetids]);

		$pages = $assertor->getRows('page', ['pagetemplateid' => $pagetemplateids]);
		if (!empty($pages))
		{
			$pageids = [];
			$pagenames = [];
			foreach ($pages AS $__page)
			{
				$pageids[] = $__page['pageid'];
				$pagenames[] = $__page['title'];
			}
			$params = [
				'doabsoluteurl' => true,
				'pageid' => $pageids,
			];
			$urlData = vB_Library::instance("page")->getUrls($params);
			/*
				getUrls() returns the following blocks:
					'all',
					'custom_pages',
					'orphans'
				'all' should include 'custom_pages' as well. Ignoring 'orphans' for now since if we want them
				we have to fetch them as a separate query.
			 */
			$string = vB::getString();
			$urls = [];
			foreach ($urlData['all']['paginated'] AS $__pageno => $__list)
			{
				foreach ($__list AS $__key => $__urldata)
				{

					$__urldata['url_htmlsafe'] = $string->htmlentities($__urldata['url']);
					$__urldata['label_htmlsafe'] = $string->htmlentities($__urldata['label']);
					/*
					From admin_sbpanel_pagelist_content template:
					<a href="{vb:var page.url}" title="{vb:var page.label}">{vb:var page.label}</a>{vb:var page.label_after_anchor}
					 */
					$urls[] = "<a href=\"{$__urldata['url_htmlsafe']}\" title=\"{$__urldata['label_htmlsafe']}\">"
						. $string->htmlentities($__urldata['label'])
						. "</a>"
						. $string->htmlentities($__urldata['label_after_anchor']);
				}
			}
			/*
				todo: There might be rare cases where we have pages but no URLs
				($excludedChannelGUIDs in vB_Api_Pages::getUrls(), and "content"
				pages like specific topic URLs, as there would be way too many)
				The warning phrase below notes that not all pages have URLs, but
				we may want to explicitly handle the case where there are NO URLs
				with a second phrase.
			 */

			$widget_info = construct_phrase(
				$vbphrase['uninstall_product_widgets_removed_on_pages_x'],
				'<li>' . implode('</li><li>', $pagenames) . '</li>',
				'<li>' . implode('</li><li>', $urls) . '</li>'
			);
		}
	}

	if (!empty($affected_children) AND !empty($widget_info))
	{
		$extra = $affected_children . "<br />" . $widget_info;
	}
	else
	{
		// If there's only one or no extra phrases, don't add an unnecessary break
		$extra = $affected_children . $widget_info;
	}


	print_delete_confirmation(
		'product',
		$vbulletin->GPC['productid'],
		'product',
		'productkill',
		'',
		0,
		$extra
	);
}

// #############################################################################

if ($_POST['do'] == 'productimport')
{
	$vbulletin->input->clean_array_gpc('p', [
		'serverfile'   => vB_Cleaner::TYPE_STR,
		'allowoverwrite' => vB_Cleaner::TYPE_BOOL
	]);

	$vbulletin->input->clean_array_gpc('f', [
		'productfile' => vB_Cleaner::TYPE_FILE
	]);
	scanVbulletinGPCFile('productfile');

	// Get realpaths.
	$serverfile = realpath($vbulletin->GPC['serverfile']);
	$tempfile = realpath($vbulletin->GPC['productfile']['tmp_name']);

	if (!$serverfile)
	{
		// If above fails, try relative path instead.
		$serverfile = realpath(DIR . DIRECTORY_SEPARATOR . $vbulletin->GPC['serverfile']);
	}

	// do not use file_exists here, under IIS it will return false in some cases
	if ($tempfile AND is_uploaded_file($tempfile))
	{
		// got an uploaded file?
		$xml = file_read($tempfile);
	}
	else if ($serverfile AND file_exists($serverfile))
	{
		// no uploaded file - got a local file?
		$xml = file_read($serverfile);
	}
	else
	{
		print_stop_message('no_file_uploaded_and_no_local_file_found_gerror');
	}

	try
	{
		$info = install_product($xml, $vbulletin->GPC['allowoverwrite']);
		//install translations if this xml file corresponds to a proper package
		//and we have an official translation.
	 	vB_Library_Functions::installProductTranslations($info['productid'], VB_PKG_PATH . '/' .  $info['productid'] . '/xml');
	}
	catch (vB_Exception_AdminStopMessage $e)
	{
		print_stop_message_exception($e);
	}

	/*
		Figure out what we want to do in the end.
		What we'd like to do is
			1. If don't need a merge, print the stop message which redirects to either the defined redirect
				for the product or the default redirect (aka the products admin page)
			2. If we do, then redirect to the merge page which will redirect to the proper redirect page
				when finished.

		As always users complicate things.  Some products want to display errors which get unreadable when
		the page automatically redirects.  We have a DISABLE_PRODUCT_REDIRECT flag which is supposed to
		simply display the stop message and not redirect.
	*/

	$file = false;
	$args = [];
	if (!defined('DISABLE_PRODUCT_REDIRECT'))
	{
		$file = 'product';
		$args = ['do' => 'product'];
	}

	// passing the full url via the query string runs afoul of some mod_rewrite rules.
	$redirectArgs = [
		'locfile' => 'product',
		'locparams' => [
			'do' => 'product',
		],
	];

	if ($info['need_merge'])
	{
		$file = 'template';
		$args = [
			'do' => 'massmerge',
			'product' => $info['productid'],
			'hash' => CP_SESSIONHASH,
			'redirect' => $redirectArgs
		];

		if (!defined('DISABLE_PRODUCT_REDIRECT'))
		{
			print_cp_redirect2('template', $args, 2, 'admincp');
		}
		else
		{
			//if we just don't define the back url we'll get a javascript "back" as default.
			//an empty string (instead of null) triggers no back button, which is what we want.
			//ugly, but it avoids rewriting a lot of the logic in print_stop_message here.
			$backurl = '';
			$url = get_admincp_url($file, $args);
			print_stop_message(['product_x_imported_need_merge', $info['productid']], $url, $backurl, true);
		}
	}
	else
	{
		$url = get_admincp_url($file, $args);
		print_stop_message(['product_x_imported',  $info['productid']], $url);
	}
}

// #############################################################################

if ($_REQUEST['do'] == 'productexport')
{
	try
	{
		$doc = get_product_export_xml($vbulletin->GPC['productid']);
	}
	catch (vB_Exception_AdminStopMessage $e)
	{
		print_stop_message_exception($e);
	}

	require_once(DIR . '/includes/functions_file.php');
	file_download($doc, "product_" . $vbulletin->GPC['productid'] . '.xml', 'text/xml');
}

// #############################################################################

if ($_REQUEST['do'] == 'extensions')
{
	print_table_start();
	print_table_header(construct_phrase($vbphrase['list_extensions_version'], $vbulletin->options['templateversion']), 7);

	print_cells_row(
		[
			$vbphrase['title'] . ' (' .
			$vbphrase['version_products'] . ')',
			$vbphrase['class'],
			$vbphrase['active'],
			$vbphrase['minver'],
			$vbphrase['maxver'],
			$vbphrase['compatible'],
			$vbphrase['order'],
		],
		true,
		false,
		0.5 // Stops the final column being right aligned
	);

	$options = vB::getDatastore()->getValue('options');

	$failed = [];
	$extensions = vB_Api_Extensions::loadAllExtensions();

	if ($extensions)
	{
		$product = '';
		foreach ($extensions AS $extn)
		{
			if (isset($extn['__failed']))
			{
				//__failed can only have one item in it, which makes having it
				//as an array of key => value pairs dubious.

				$value = reset($extn['__failed']);
				$dir = key($extn['__failed']);
				$failed[] = [
					'dir'  => $dir,
					'file' => $value[0]
				];

				continue;
			}

			if ($product != $extn['product'])
			{
				print_description_row(
					$vbphrase['product'] . ': ' . $extn['package'],
					false,
					7,
					'boldrow'
				);

				$product = $extn['product'];
			}

			if (!$options['enablehooks'] OR defined('DISABLE_HOOKS'))
			{
				$extn['enabled'] = false;
			}

			$title = htmlspecialchars_uni($extn['title']);
			$title = $extn['enabled'] ? $title : "<strike>$title</strike>";

			$enabled = $extn['enabled'] ? $vbphrase['yes'] : ($extn['dependancy'] ? $vbphrase['no_dep'] : $vbphrase['no']);

			print_cells_row(
				[
					vB_Library_Admin::buildElementCell('', $title, 0, false, '', '', '', $extn['version'], $extn['developer']),
					vB_Library_Admin::buildDisplayCell($extn['class']),
					vB_Library_Admin::buildDisplayCell($enabled, ($extn['enabled'] AND $extn['compatible'])),
					vB_Library_Admin::buildDisplayCell($extn['minver']),
					vB_Library_Admin::buildDisplayCell($extn['maxver']),
					vB_Library_Admin::buildDisplayCell($extn['compatible'] ? $vbphrase['yes'] : $vbphrase['no'], $extn['compatible']),
					vB_Library_Admin::buildDisplayCell($extn['order']),
				],
				false,
				false,
				0.5
			);
		}

		if (!empty($failed))
		{
			print_table_footer();
			print_table_start();
			print_table_header($vbphrase['failed_extensions'], 2);

			print_cells_row(
				[
					$vbphrase['directory'],
					$vbphrase['filename_gcpglobal'],
				],
				true,
				false,
				0.5
			);

			foreach ($failed AS $info)
			{
				print_cells_row(
					[
						$info['dir'],
						$info['file'],
					],
					false,
					false,
					0.5
				);
			}
		}

		print_table_footer();
	}
	else
	{
		print_description_row(
			$vbphrase['no_extensions'],
			false,
			7,
			'boldrow'
		);

		print_table_footer();
	}

	//display product particulars

	print_table_start();
	print_table_header($vbphrase['product_list'], 5);

	print_cells_row(
		[
			$vbphrase['title'].' ('.$vbphrase['version_products'].')',
			$vbphrase['product_class'],
			$vbphrase['minver'],
			$vbphrase['maxver'],
			$vbphrase['api_classes'],
		],
		true,
		false,
		0.5
	);


	$product_meta = $assertor->getRows('product', [], false, 'productid');

	$products = vB::getProducts();
	$api_classes = $products->getApiClassesByProduct();

	$productlist = $products->getProductObjects();
	$disabledproductslist = $products->getDisabledProductObjects();

	$api_classes = $products->getApiClassesByProduct();

	function display_product_row($object, $meta, $api_classes, $enabled)
	{
		$packagetitle = $meta['title'];
		$version = $meta['version'];
		if (!$enabled)
		{
			$packagetitle = '<s>' . $packagetitle . '</s>';
			$version = '<s>' . $version . '</s>';
		}

		$classes = [];
		foreach ($api_classes AS $api_class)
		{
			$classes[] = $api_class['classname'];
		}

		$classes = implode('<br/>', array_unique($classes));
		$minVersion = (empty($object->vbMinVersion) ? vB_Phrase::fetchSinglePhrase('none') : $object->vbMinVersion);
		$maxVersion = (empty($object->vbMaxVersion) ? vB_Phrase::fetchSinglePhrase('none') : $object->vbMaxVersion);

		print_cells_row(
			[
				vB_Library_Admin::buildElementCell('', $packagetitle, 0, false, '', '', '', $version, $meta['description']),
				vB_Library_Admin::buildDisplayCell(get_class($object)),
				vB_Library_Admin::buildDisplayCell($minVersion),
				vB_Library_Admin::buildDisplayCell($maxVersion),
				vB_Library_Admin::buildDisplayCell($classes)
			],
			false,
			false,
			0.5
		);
	}

	if ($productlist OR $disabledproductslist)
	{
		foreach ($productlist AS $packagename => $hookproduct)
		{
			$classes = (isset($api_classes[$packagename]) ? $api_classes[$packagename] : []);
			display_product_row($hookproduct, $product_meta[$packagename], $classes, $hooksEnabled);
		}

		foreach ($disabledproductslist AS $packagename => $hookproduct)
		{
			$classes = (isset($api_classes[$packagename]) ? $api_classes[$packagename] : []);
			display_product_row($hookproduct, $product_meta[$packagename], $classes, false);
		}
	}
	else
	{
		print_description_row(
			$vbphrase['no_products'],
			false,
			5,
			'boldrow'
		);
	}

	print_table_footer();
	//end products

	print_table_start();
	print_table_header($vbphrase['product_hook_list'], 3);

	print_cells_row(
		[
			$vbphrase['hook_classes'],
			$vbphrase['order'],
			$vbphrase['hook_list'],
		],
		true,
		false,
		0.5
	);

	function display_product_hooks($object, $meta, $enabled, $phrases)
	{
		if (empty($object->hookClasses))
		{
			return;
		}

		$title = htmlspecialchars_uni($meta['title']);
		$title = $enabled ? $title : "<strike>$title</strike>";

		print_description_row(
			$phrases['product'] . ': ' . $title,
			false,
			4,
			'boldrow'
		);

		foreach ($object->hookClasses AS $hook_class)
		{
			$methodslist = [];
			if (class_exists($hook_class))
			{
				$methods = get_class_methods($hook_class);
				$methods = (empty($methods) ? [] : $methods);

				foreach ($methods AS $method)
				{
					$methodslist[] = $method;
				}

				$classmethods = implode('<br/>', array_unique($methodslist));

				$classname= $enabled ? $hook_class : "<strike>$hook_class</strike>";
				$order = isset($hook_class::$order) ? $hook_class::$order : 10;
			}
			else
			{
				$classname = '<s>' . $hook_class . '</s>';
				$order = '';
				$classmethods = '';
			}

			print_cells_row(
				[
					vB_Library_Admin::buildDisplayCell($classname),
					vB_Library_Admin::buildDisplayCell($order),
					vB_Library_Admin::buildDisplayCell($classmethods, false, true),
				],
				false,
				false,
				0.5
			);
		}
	}

	if ($productlist OR $disabledproductslist)
	{
		foreach ($productlist AS $packagename => $hookproduct)
		{
			display_product_hooks($hookproduct, $product_meta[$packagename], $hooksEnabled, $vbphrase);
		}

		foreach ($disabledproductslist AS $packagename => $hookproduct)
		{
			display_product_hooks($hookproduct, $product_meta[$packagename], false, $vbphrase);
		}
	}
	else
	{
		print_description_row(
			$vbphrase['no_hook_products'],
			false,
			3,
			'boldrow'
		);
	}

	print_table_footer();
}

print_cp_footer();

/*=========================================================================*\
|| #######################################################################
|| # Downloaded: 13:27, Sun Aug 30th 2026
|| # CVS: $RCSfile$ - $Revision: 122851 $
|| #######################################################################
\*=========================================================================*/
