PageRenderTime 84ms CodeModel.GetById 4ms app.highlight 62ms RepoModel.GetById 1ms app.codeStats 1ms

/extensions/ext.fieldframe.php

https://github.com/pixelandtonic/fieldframe
PHP | 3181 lines | 2197 code | 341 blank | 643 comment | 238 complexity | ec07d647ec7facfa36ff39cf6d7c940c MD5 | raw file
   1<?php
   2
   3if ( ! defined('EXT')) exit('Invalid file request');
   4
   5// define FF constants
   6// (used by Fieldframe and Fieldframe_Main)
   7if ( ! defined('FF_CLASS'))
   8{
   9	define('FF_CLASS',   'Fieldframe');
  10	define('FF_NAME',    'FieldFrame');
  11	define('FF_VERSION', '1.4.5');
  12}
  13
  14
  15/**
  16 * FieldFrame Class
  17 *
  18 * This extension provides a framework for ExpressionEngine fieldtype development.
  19 *
  20 * @package   FieldFrame
  21 * @author    Brandon Kelly <brandon@pixelandtonic.com>
  22 * @copyright Copyright (c) 2011 Pixel & Tonic, Inc
  23 * @license   http://creativecommons.org/licenses/by-sa/3.0/ Attribution-Share Alike 3.0 Unported
  24 */
  25class Fieldframe {
  26
  27	var $name           = FF_NAME;
  28	var $version        = FF_VERSION;
  29	var $description    = 'Fieldtype Framework';
  30	var $settings_exist = 'y';
  31	var $docs_url       = 'http://pixelandtonic.com/fieldframe/docs';
  32
  33	var $hooks = array(
  34		'sessions_start' => array('priority' => 1),
  35
  36		// Edit Field Form
  37		'publish_admin_edit_field_type_pulldown',
  38		'publish_admin_edit_field_type_cellone',
  39		'publish_admin_edit_field_type_celltwo',
  40		'publish_admin_edit_field_extra_row',
  41		'publish_admin_edit_field_format',
  42		'publish_admin_edit_field_js',
  43
  44		// Field Manager
  45		'show_full_control_panel_start',
  46		'show_full_control_panel_end',
  47
  48		// Entry Form
  49		'publish_form_field_unique',
  50		'submit_new_entry_start',
  51		'submit_new_entry_end',
  52		'publish_form_start',
  53
  54		// SAEF
  55		'weblog_standalone_form_start',
  56		'weblog_standalone_form_end',
  57
  58		// Templates
  59		'weblog_entries_tagdata' => array('priority' => 1),
  60
  61		// LG Addon Updater
  62		'lg_addon_update_register_source',
  63		'lg_addon_update_register_addon'
  64	);
  65
  66	/**
  67	 * FieldFrame Class Constructor
  68	 *
  69	 * @param array  $settings
  70	 */
  71	function __construct($settings=array())
  72	{
  73		$this->_init_main($settings);
  74	}
  75
  76	/**
  77	 * Activate Extension
  78	 */
  79	function activate_extension()
  80	{
  81		global $DB;
  82
  83		// require PHP 5
  84		if (phpversion() < 5) return;
  85
  86		// Get settings
  87		$query = $DB->query('SELECT settings FROM exp_extensions WHERE class = "'.FF_CLASS.'" AND settings != "" LIMIT 1');
  88		$settings = $query->num_rows ? $this->_unserialize($query->row['settings']) : array();
  89		$this->_init_main($settings, TRUE);
  90
  91		global $FF;
  92		$FF->activate_extension($settings);
  93	}
  94
  95	/**
  96	 * Update Extension
  97	 *
  98	 * @param string  $current  Previous installed version of the extension
  99	 */
 100	function update_extension($current='')
 101	{
 102		global $DB;
 103
 104		if ( ! $current OR $current == FF_VERSION)
 105		{
 106			return FALSE;
 107		}
 108
 109		if ($current < '1.1.3')
 110		{
 111			// no longer saving settings on a per-site basis
 112
 113			$sql = array();
 114
 115			// no more per-site FF settings
 116			$query = $DB->query('SELECT settings FROM exp_extensions WHERE class = "'.FF_CLASS.'" AND settings != "" LIMIT 1');
 117			if ($query->row)
 118			{
 119				$settings = array_shift(Fieldframe_Main::_unserialize($query->row['settings']));
 120				$sql[] = $DB->update_string('exp_extensions', array('settings' => Fieldframe_Main::_serialize($settings)), 'class = "'.FF_CLASS.'"');
 121			}
 122
 123			// collect conversion info
 124			$query = $DB->query('SELECT * FROM exp_ff_fieldtypes ORDER BY enabled DESC, site_id ASC');
 125			$firsts = array();
 126			$conversions = array();
 127			foreach ($query->result as $ftype)
 128			{
 129				if ( ! isset($firsts[$ftype['class']]))
 130				{
 131					$firsts[$ftype['class']] = $ftype['fieldtype_id'];
 132				}
 133				else
 134				{
 135					$conversions[$ftype['fieldtype_id']] = $firsts[$ftype['class']];
 136				}
 137			}
 138
 139			if ($conversions)
 140			{
 141				// remove duplicate ftype rows in exp_ff_fieldtypes
 142				$sql[] = 'DELETE FROM exp_ff_fieldtypes WHERE fieldtype_id IN ('.implode(',', array_keys($conversions)).')';
 143
 144				// update field_type's in exp_weblog_fields
 145				foreach($conversions as $old_id => $new_id)
 146				{
 147					$sql[] = $DB->update_string('exp_weblog_fields', array('field_type' => 'ftype_id_'.$new_id), 'field_type = "ftype_id_'.$old_id.'"');
 148				}
 149			}
 150
 151			// remove site_id column from exp_ff_fieldtypes
 152			$sql[] = 'ALTER TABLE exp_ff_fieldtypes DROP COLUMN site_id';
 153
 154			// apply changes
 155			foreach($sql as $query)
 156			{
 157				$DB->query($query);
 158			}
 159		}
 160
 161		if ($current < '1.1.0')
 162		{
 163			// hooks have changed, so go through
 164			// the whole activate_extension() process
 165			$this->activate_extension();
 166		}
 167		else
 168		{
 169			// just update the version #s
 170			$DB->query('UPDATE exp_extensions
 171			              SET version = "'.FF_VERSION.'"
 172			              WHERE class = "'.FF_CLASS.'"');
 173		}
 174	}
 175
 176	/**
 177	 * Disable Extension
 178	 */
 179	function disable_extension()
 180	{
 181		global $DB;
 182		$DB->query($DB->update_string('exp_extensions', array('enabled' => 'n'), 'class = "'.FF_CLASS.'"'));
 183	}
 184
 185	/**
 186	 * Settings Form
 187	 *
 188	 * @param array  $settings
 189	 */
 190	function settings_form($settings=array())
 191	{
 192		$this->_init_main($settings);
 193
 194		global $FF;
 195		$FF->settings_form();
 196	}
 197
 198	function save_settings()
 199	{
 200		$this->_init_main(array(), TRUE);
 201
 202		global $FF;
 203		$FF->save_settings();
 204	}
 205
 206	/**
 207	 * Initialize Main class
 208	 *
 209	 * @param  array  $settings
 210	 * @access private
 211	 */
 212	function _init_main($settings, $force=FALSE)
 213	{
 214		global $SESS, $FF;
 215
 216		if ( ! isset($FF) OR $force)
 217		{
 218			$FF = new Fieldframe_Main($settings, $this->hooks);
 219		}
 220	}
 221
 222	/**
 223	 * __call Magic Method
 224	 *
 225	 * Routes calls to missing methods to $FF
 226	 *
 227	 * @param string  $method  Name of the missing method
 228	 * @param array   $args    Arguments sent to the missing method
 229	 */
 230	function __call($method, $args)
 231	{
 232		global $FF, $EXT;
 233
 234		if (isset($FF))
 235		{
 236			if (method_exists($FF, $method))
 237			{
 238				return call_user_func_array(array(&$FF, $method), $args);
 239			}
 240			else if (substr($method, 0, 13) == 'forward_hook:')
 241			{
 242				$ext = explode(':', $method); // [ forward_hook, hook name, priority ]
 243				return call_user_func_array(array(&$FF, 'forward_hook'), array($ext[1], $ext[2], $args));
 244			}
 245		}
 246
 247		return FALSE;
 248	}
 249
 250}
 251
 252/**
 253 * FieldFrame_Main Class
 254 *
 255 * Provides the core extension logic + hooks
 256 *
 257 * @package   FieldFrame
 258 */
 259class Fieldframe_Main {
 260
 261	function log()
 262	{
 263		foreach(func_get_args() as $var)
 264		{
 265			if (is_string($var))
 266			{
 267				echo "<code style='display:block; margin:0; padding:5px 10px;'>{$var}</code>";
 268			}
 269			else
 270			{
 271				echo '<pre style="display:block; margin:0; padding:5px 10px; width:auto">';
 272				print_r($var);
 273				echo '</pre>';
 274			}
 275		}
 276	}
 277
 278	var $errors = array();
 279	var $postponed_saves = array();
 280	var $snippets = array('head' => array(), 'body' => array());
 281
 282	var $saef = FALSE;
 283
 284	/**
 285	 * FieldFrame_Main Class Initialization
 286	 *
 287	 * @param array  $settings
 288	 */
 289	function __construct($settings, $hooks)
 290	{
 291		global $SESS, $DB;
 292
 293		$this->hooks = $hooks;
 294
 295		// merge settings with defaults
 296		$default_settings = array(
 297			'fieldtypes_url' => '',
 298			'fieldtypes_path' => '',
 299			'check_for_updates' => 'y'
 300		);
 301		$this->settings = array_merge($default_settings, $settings);
 302
 303		// set the FT_PATH and FT_URL constants
 304		$this->_define_constants();
 305	}
 306
 307	/**
 308	 * Define Constants
 309	 *
 310	 * @access private
 311	 */
 312	function _define_constants()
 313	{
 314		global $PREFS;
 315
 316		if ( ! defined('FT_PATH') AND ($ft_path = isset($PREFS->core_ini['ft_path']) ? $PREFS->core_ini['ft_path'] : $this->settings['fieldtypes_path']))
 317		{
 318			define('FT_PATH', $ft_path);
 319		}
 320
 321		if ( ! defined('FT_URL') AND ($ft_path = isset($PREFS->core_ini['ft_url']) ? $PREFS->core_ini['ft_url'] : $this->settings['fieldtypes_url']))
 322		{
 323			define('FT_URL', $ft_path);
 324			$this->snippets['body'][] = '<script type="text/javascript">;FT_URL = "'.FT_URL.'";</script>';
 325		}
 326	}
 327
 328	/**
 329	 * Array Ascii to Entities
 330	 *
 331	 * @access private
 332	 */
 333	function _array_ascii_to_entities($vals)
 334	{
 335		if (is_array($vals))
 336		{
 337			foreach ($vals as &$val)
 338			{
 339				$val = $this->_array_ascii_to_entities($val);
 340			}
 341		}
 342		else if (! is_numeric($vals))
 343		{
 344			global $REGX;
 345			$vals = $REGX->ascii_to_entities($vals);
 346		}
 347
 348		return $vals;
 349	}
 350
 351	/**
 352	 * Array Ascii to Entities
 353	 *
 354	 * @access private
 355	 */
 356	function _array_entities_to_ascii($vals)
 357	{
 358		if (is_array($vals))
 359		{
 360			foreach ($vals as &$val)
 361			{
 362				$val = $this->_array_entities_to_ascii($val);
 363			}
 364		}
 365		else
 366		{
 367			global $REGX;
 368			$vals = $REGX->entities_to_ascii($vals);
 369		}
 370
 371		return $vals;
 372	}
 373
 374	/**
 375	 * Serialize
 376	 *
 377	 * @access private
 378	 */
 379	function _serialize($vals)
 380	{
 381		global $PREFS;
 382
 383		if ($PREFS->ini('auto_convert_high_ascii') == 'y')
 384		{
 385			$vals = $this->_array_ascii_to_entities($vals);
 386		}
 387
 388     	return addslashes(serialize($vals));
 389	}
 390
 391	/**
 392	 * Unserialize
 393	 *
 394	 * @access private
 395	 */
 396	function _unserialize($vals, $convert=TRUE)
 397	{
 398		global $REGX, $PREFS;
 399
 400		if ($vals && is_string($vals) && preg_match('/^(i|s|a|o|d):(.*);/si', $vals) && ($tmp_vals = @unserialize($vals)) !== FALSE)
 401		{
 402			$vals = $REGX->array_stripslashes($tmp_vals);
 403
 404			if ($convert AND $PREFS->ini('auto_convert_high_ascii') == 'y')
 405			{
 406				$vals = $this->_array_entities_to_ascii($vals);
 407			}
 408		}
 409
 410     	return $vals;
 411	}
 412
 413	/**
 414	 * Get Fieldtypes
 415	 *
 416	 * @return array  All enabled FF fieldtypes, indexed by class name
 417	 * @access private
 418	 */
 419	function _get_ftypes()
 420	{
 421		global $DB;
 422
 423		if ( ! isset($this->ftypes))
 424		{
 425			$this->ftypes = array();
 426
 427			// get enabled fields from the DB
 428			$query = $DB->query('SELECT * FROM exp_ff_fieldtypes WHERE enabled = "y"');
 429
 430			if ($query->num_rows)
 431			{
 432				foreach($query->result as $row)
 433				{
 434					if (($ftype = $this->_init_ftype($row)) !== FALSE)
 435					{
 436						$this->ftypes[] = $ftype;
 437					}
 438				}
 439
 440				$this->_sort_ftypes($this->ftypes);
 441			}
 442		}
 443
 444		return $this->ftypes;
 445	}
 446
 447	function _get_ftype($class_name)
 448	{
 449		$ftypes = $this->_get_ftypes();
 450		return isset($ftypes[$class_name])
 451		  ?  $ftypes[$class_name]
 452		  :  FALSE;
 453	}
 454
 455	/**
 456	 * Get All Installed Fieldtypes
 457	 *
 458	 * @return array  All installed FF fieldtypes, indexed by class name
 459	 * @access private
 460	 */
 461	function _get_all_installed_ftypes()
 462	{
 463		$ftypes = array();
 464
 465		if (defined('FT_PATH'))
 466		{
 467			if ( $fp = @opendir(FT_PATH))
 468			{
 469				// iterate through the field folder contents
 470				while (($file = readdir($fp)) !== FALSE)
 471				{
 472					// skip hidden/navigational files
 473					if (substr($file, 0, 1) == '.') continue;
 474
 475					// is this a directory, and does a ftype file exist inside it?
 476					if (is_dir(FT_PATH.$file) AND is_file(FT_PATH.$file.'/ft.'.$file.EXT))
 477					{
 478						if (($ftype = $this->_init_ftype($file, FALSE)) !== FALSE)
 479						{
 480							$ftypes[] = $ftype;
 481						}
 482					}
 483				}
 484				closedir($fp);
 485
 486				$this->_sort_ftypes($ftypes);
 487			}
 488			else
 489			{
 490				$this->errors[] = 'bad_ft_path';
 491			}
 492		}
 493
 494		return $ftypes;
 495	}
 496
 497	/**
 498	 * Sort Fieldtypes
 499	 *
 500	 * @param  array  $ftypes  the array of fieldtypes
 501	 * @access private
 502	 */
 503	function _sort_ftypes(&$ftypes)
 504	{
 505		$ftypes_by_name = array();
 506		while($ftype = array_shift($ftypes))
 507		{
 508			$ftypes_by_name[$ftype->info['name']] = $ftype;
 509		}
 510		ksort($ftypes_by_name);
 511		foreach($ftypes_by_name as $ftype)
 512		{
 513			$ftypes[$ftype->_class_name] = $ftype;
 514		}
 515	}
 516
 517	/**
 518	 * Get Fieldtypes Indexed By Field ID
 519	 *
 520	 * @return array  All enabled FF fieldtypes, indexed by the weblog field ID they're used in.
 521	 *                Strong possibility that there will be duplicate fieldtypes in here,
 522	 *                but it's not a big deal because they're just object references
 523	 * @access private
 524	 */
 525	function _get_fields()
 526	{
 527		global $DB, $REGX;
 528
 529		if ( ! isset($this->ftypes_by_field_id))
 530		{
 531			$this->ftypes_by_field_id = array();
 532
 533			// get the fieldtypes
 534			if ($ftypes = $this->_get_ftypes())
 535			{
 536				// index them by ID rather than class
 537				$ftypes_by_id = array();
 538				foreach($ftypes as $class_name => $ftype)
 539				{
 540					$ftypes_by_id[$ftype->_fieldtype_id] = $ftype;
 541				}
 542
 543				// get the field info
 544				$query = $DB->query("SELECT field_id, site_id, field_name, field_type, ff_settings FROM exp_weblog_fields
 545				                       WHERE field_type IN ('ftype_id_".implode("', 'ftype_id_", array_keys($ftypes_by_id))."')");
 546				if ($query->num_rows)
 547				{
 548					// sort the current site's fields on top
 549					function sort_fields($a, $b)
 550					{
 551						global $PREFS;
 552						if ($a['site_id'] == $b['site_id']) return 0;
 553						if ($a['site_id'] == $PREFS->ini('site_id')) return 1;
 554						if ($b['site_id'] == $PREFS->ini('site_id')) return -1;
 555						return 0;
 556					}
 557					usort($query->result, 'sort_fields');
 558
 559					foreach($query->result as $row)
 560					{
 561						$ftype_id = substr($row['field_type'], 9);
 562						$this->ftypes_by_field_id[$row['field_id']] = array(
 563							'name' => $row['field_name'],
 564							'ftype' => $ftypes_by_id[$ftype_id],
 565							'settings' => array_merge(
 566							                           (isset($ftypes_by_id[$ftype_id]->default_field_settings) ? $ftypes_by_id[$ftype_id]->default_field_settings : array()),
 567							                           ($row['ff_settings'] ? (array)$this->_unserialize($row['ff_settings']) : array())
 568							                          )
 569						);
 570					}
 571				}
 572			}
 573		}
 574
 575		return $this->ftypes_by_field_id;
 576	}
 577
 578	/**
 579	 * Initialize Fieldtype
 580	 *
 581	 * @param  mixed   $ftype  fieldtype's class name or its row in exp_ff_fieldtypes
 582	 * @return object  Initialized fieldtype object
 583	 * @access private
 584	 */
 585	function _init_ftype($ftype, $req_strict=TRUE)
 586	{
 587		global $DB, $REGX, $LANG;
 588
 589		$file = is_array($ftype) ? $ftype['class'] : $ftype;
 590		$class_name = ucfirst($file);
 591
 592		if ( ! class_exists($class_name) && ! class_exists($class_name.'_ft'))
 593		{
 594			// import the file
 595			@include(FT_PATH.$file.'/ft.'.$file.EXT);
 596
 597			// skip if the class doesn't exist
 598			if ( ! class_exists($class_name) && ! class_exists($class_name.'_ft'))
 599			{
 600				return FALSE;
 601			}
 602		}
 603
 604		$_class_name = $class_name . (class_exists($class_name) ? '' : '_ft');
 605		$OBJ = new $_class_name();
 606
 607		// is this a FieldFrame fieldtype?
 608		if ( ! isset($OBJ->_fieldframe)) return FALSE;
 609
 610		$OBJ->_class_name = $file;
 611		$OBJ->_is_new     = FALSE;
 612		$OBJ->_is_enabled = FALSE;
 613		$OBJ->_is_updated = FALSE;
 614
 615		// settings
 616		$OBJ->site_settings = isset($OBJ->default_site_settings)
 617		  ?  $OBJ->default_site_settings
 618		  :  array();
 619
 620		// info
 621		if ( ! isset($OBJ->info)) $OBJ->info = array();
 622
 623		// requirements
 624		if ( ! isset($OBJ->_requires)) $OBJ->_requires = array();
 625		if (isset($OBJ->requires))
 626		{
 627			// PHP
 628			if (isset($OBJ->requires['php']) AND phpversion() < $OBJ->requires['php'])
 629			{
 630				if ($req_strict) return FALSE;
 631				else $OBJ->_requires['PHP'] = $OBJ->requires['php'];
 632			}
 633
 634			// ExpressionEngine
 635			if (isset($OBJ->requires['ee']) AND APP_VER < $OBJ->requires['ee'])
 636			{
 637				if ($req_strict) return FALSE;
 638				else $OBJ->_requires['ExpressionEngine'] = $OBJ->requires['ee'];
 639			}
 640
 641			// ExpressionEngine Build
 642			if (isset($OBJ->requires['ee_build']) AND APP_BUILD < $OBJ->requires['ee_build'])
 643			{
 644				if ($req_strict) return FALSE;
 645				else
 646				{
 647					$req = substr(strtolower($LANG->line('build')), 0, -1).' '.$OBJ->requires['ee_build'];
 648					if (isset($OBJ->_requires['ExpressionEngine'])) $OBJ->_requires['ExpressionEngine'] .= ' '.$req;
 649					else $OBJ->_requires['ExpressionEngine'] = $req;
 650				}
 651			}
 652
 653			// FieldFrame
 654			if (isset($OBJ->requires['ff']) AND FF_VERSION < $OBJ->requires['ff'])
 655			{
 656				if ($req_strict) return FALSE;
 657				else $OBJ->_requires[FF_NAME] = $OBJ->requires['ff'];
 658			}
 659
 660			// CP jQuery
 661			if (isset($OBJ->requires['cp_jquery']))
 662			{
 663				if ( ! isset($OBJ->requires['ext']))
 664				{
 665					$OBJ->requires['ext'] = array();
 666				}
 667				$OBJ->requires['ext'][] = array('class' => 'Cp_jquery', 'name' => 'CP jQuery', 'url' => 'http://www.ngenworks.com/software/ee/cp_jquery/', 'version' => $OBJ->requires['cp_jquery']);
 668			}
 669
 670			// Extensions
 671			if (isset($OBJ->requires['ext']))
 672			{
 673				if ( ! isset($this->req_ext_versions))
 674				{
 675					$this->req_ext_versions = array();
 676				}
 677				foreach($OBJ->requires['ext'] as $ext)
 678				{
 679					if ( ! isset($this->req_ext_versions[$ext['class']]))
 680					{
 681						$ext_query = $DB->query('SELECT version FROM exp_extensions
 682						                           WHERE class="'.$ext['class'].'"'
 683						                           . (isset($ext['version']) ? ' AND version >= "'.$ext['version'].'"' : '')
 684						                           . ' AND enabled = "y"
 685						                           ORDER BY version DESC
 686						                           LIMIT 1');
 687						$this->req_ext_versions[$ext['class']] = $ext_query->row
 688						  ?  $ext_query->row['version']
 689						  :  '';
 690					}
 691					if ($this->req_ext_versions[$ext['class']] < $ext['version'])
 692					{
 693						if ($req_strict) return FALSE;
 694						else
 695						{
 696							$name = isset($ext['name']) ? $ext['name'] : ucfirst($ext['class']);
 697							if ($ext['url']) $name = '<a href="'.$ext['url'].'">'.$name.'</a>';
 698							$OBJ->_requires[$name] = $ext['version'];
 699						}
 700					}
 701				}
 702			}
 703		}
 704
 705		if ( ! isset($OBJ->info['name'])) $OBJ->info['name'] = ucwords(str_replace('_', ' ', $class_name));
 706		if ( ! isset($OBJ->info['version'])) $OBJ->info['version'] = '';
 707		if ( ! isset($OBJ->info['desc'])) $OBJ->info['desc'] = '';
 708		if ( ! isset($OBJ->info['docs_url'])) $OBJ->info['docs_url'] = '';
 709		if ( ! isset($OBJ->info['author'])) $OBJ->info['author'] = '';
 710		if ( ! isset($OBJ->info['author_url'])) $OBJ->info['author_url'] = '';
 711		if ( ! isset($OBJ->info['versions_xml_url'])) $OBJ->info['versions_xml_url'] = '';
 712		if ( ! isset($OBJ->info['no_lang'])) $OBJ->info['no_lang'] = FALSE;
 713
 714		if ( ! isset($OBJ->hooks)) $OBJ->hooks = array();
 715		if ( ! isset($OBJ->postpone_saves)) $OBJ->postpone_saves = FALSE;
 716
 717		// do we already know about this fieldtype?
 718		if (is_string($ftype))
 719		{
 720			$query = $DB->query('SELECT * FROM exp_ff_fieldtypes WHERE class = "'.$file.'"');
 721			$ftype = $query->row;
 722		}
 723
 724		if ($ftype)
 725		{
 726			$OBJ->_fieldtype_id = $ftype['fieldtype_id'];
 727
 728			if ($ftype['enabled'] == 'y')
 729			{
 730				$OBJ->_is_enabled = TRUE;
 731			}
 732
 733			if ($ftype['settings'])
 734			{
 735				if (is_array($ftype_settings = $this->_unserialize($ftype['settings'])))
 736				{
 737					$OBJ->site_settings = array_merge($OBJ->site_settings, $ftype_settings);
 738				}
 739			}
 740
 741			// new version?
 742			if ($OBJ->info['version'] != $ftype['version'])
 743			{
 744				$OBJ->_is_updated = TRUE;
 745
 746				// update exp_ff_fieldtypes
 747				$DB->query($DB->update_string('exp_ff_fieldtypes',
 748				                              array('version' => $OBJ->info['version']),
 749				                              'fieldtype_id = "'.$ftype['fieldtype_id'].'"'));
 750
 751				// update the hooks
 752				$this->_insert_ftype_hooks($OBJ);
 753
 754				// call update()
 755				if (method_exists($OBJ, 'update'))
 756				{
 757					$OBJ->update($ftype['version']);
 758				}
 759			}
 760		}
 761		else
 762		{
 763			$OBJ->_is_new = TRUE;
 764		}
 765
 766		return $OBJ;
 767	}
 768
 769	/**
 770	 * Insert Fieldtype Hooks
 771	 *
 772	 * @access private
 773	 */
 774	function _insert_ftype_hooks($ftype)
 775	{
 776		global $DB, $FF;
 777
 778		// remove any existing hooks from exp_ff_fieldtype_hooks
 779		$DB->query('DELETE FROM exp_ff_fieldtype_hooks
 780		              WHERE class = "'.$ftype->_class_name.'"');
 781
 782		// (re)insert the hooks
 783		if ($ftype->hooks)
 784		{
 785			foreach($ftype->hooks as $hook => $data)
 786			{
 787				if (is_string($data))
 788				{
 789					$hook = $data;
 790					$data = array();
 791				}
 792
 793				// exp_ff_fieldtype_hooks
 794				$data = array_merge(array('method' => $hook, 'priority' => 10), $data, array('hook' => $hook, 'class' => $ftype->_class_name));
 795				$DB->query($DB->insert_string('exp_ff_fieldtype_hooks', $data));
 796
 797				// exp_extensions
 798				$hooks_q = $DB->query('SELECT extension_id FROM exp_extensions WHERE class = "'.FF_CLASS.'" AND hook = "'.$hook.'" AND priority = "'.$data['priority'].'"');
 799				if ( ! $hooks_q->num_rows)
 800				{
 801					$ext_data = array('class' => FF_CLASS, 'method' => 'forward_hook:'.$hook.':'.$data['priority'], 'hook' => $hook, 'settings' => '', 'priority' => $data['priority'], 'version' => FF_VERSION, 'enabled' => 'y');
 802					$DB->query($DB->insert_string('exp_extensions', $ext_data));
 803				}
 804			}
 805		}
 806
 807		// reset cached hooks array
 808		$this->_get_ftype_hooks(TRUE);
 809	}
 810
 811	function _get_ftype_hooks($reset=FALSE)
 812	{
 813		global $DB;
 814
 815		if ($reset OR ! isset($this->ftype_hooks))
 816		{
 817			$this->ftype_hooks = array();
 818
 819			$hooks_q = $DB->query('SELECT * FROM exp_ff_fieldtype_hooks');
 820			foreach($hooks_q->result as $hook_r)
 821			{
 822				$this->ftype_hooks[$hook_r['hook']][$hook_r['priority']][$hook_r['class']] = $hook_r['method'];
 823			}
 824		}
 825
 826		return $this->ftype_hooks;
 827	}
 828
 829	/**
 830	 * Group Inputs
 831	 *
 832	 * @param  string  $name_prefix  The Fieldtype ID
 833	 * @param  string  $settings     The Fieldtype settings
 834	 * @return string  The modified settings
 835	 * @access private
 836	 */
 837	function _group_inputs($name_prefix, $settings)
 838	{
 839		return preg_replace('/(name=([\'\"]))([^\'"\[\]]+)([^\'"]*)(\2)/i', '$1'.$name_prefix.'[$3]$4$5', $settings);
 840	}
 841
 842	/**
 843	 * Group Fieldtype Inputs
 844	 *
 845	 * @param  string  $ftype_id  The Fieldtype ID
 846	 * @param  string  $settings  The Fieldtype settings
 847	 * @return string  The modified settings
 848	 * @access private
 849	 */
 850	function _group_ftype_inputs($ftype_id, $settings)
 851	{
 852		return $this->_group_inputs('ftype['.$ftype_id.']', $settings);
 853	}
 854
 855	/**
 856	 * Activate Extension
 857	 */
 858	function activate_extension($settings)
 859	{
 860		global $DB;
 861
 862		// Delete old hooks
 863		$DB->query('DELETE FROM exp_extensions
 864		              WHERE class = "'.FF_CLASS.'"');
 865
 866		// Add new extensions
 867		$hook_tmpl = array(
 868			'class'    => FF_CLASS,
 869			'settings' => $this->_serialize($settings),
 870			'priority' => 10,
 871			'version'  => FF_VERSION,
 872			'enabled'  => 'y'
 873		);
 874
 875		foreach($this->hooks as $hook => $data)
 876		{
 877			if (is_string($data))
 878			{
 879				$hook = $data;
 880				$data = array();
 881			}
 882			$data = array_merge($hook_tmpl, array('hook' => $hook, 'method' => $hook), $data);
 883			$DB->query($DB->insert_string('exp_extensions', $data));
 884		}
 885
 886		// exp_ff_fieldtypes
 887		if ( ! $DB->table_exists('exp_ff_fieldtypes'))
 888		{
 889			$DB->query("CREATE TABLE exp_ff_fieldtypes (
 890			              `fieldtype_id` int(10) unsigned NOT NULL auto_increment,
 891			              `class`        varchar(50)      NOT NULL default '',
 892			              `version`      varchar(10)      NOT NULL default '',
 893			              `settings`     text             NOT NULL default '',
 894			              `enabled`      char(1)          NOT NULL default 'n',
 895			              PRIMARY KEY (`fieldtype_id`)
 896			            )");
 897		}
 898
 899		// exp_ff_fieldtype_hooks
 900		if ( ! $DB->table_exists('exp_ff_fieldtype_hooks'))
 901		{
 902			$DB->query("CREATE TABLE exp_ff_fieldtype_hooks (
 903			              `hook_id`  int(10) unsigned NOT NULL auto_increment,
 904			              `class`    varchar(50)      NOT NULL default '',
 905			              `hook`     varchar(50)      NOT NULL default '',
 906			              `method`   varchar(50)      NOT NULL default '',
 907			              `priority` int(2)           NOT NULL DEFAULT '10',
 908			              PRIMARY KEY (`hook_id`)
 909			            )");
 910		}
 911
 912		// exp_weblog_fields.ff_settings
 913		$query = $DB->query('SHOW COLUMNS FROM `'.$DB->prefix.'weblog_fields` LIKE "ff_settings"');
 914		if ( ! $query->num_rows)
 915		{
 916			$DB->query('ALTER TABLE `'.$DB->prefix.'weblog_fields` ADD COLUMN `ff_settings` text NOT NULL');
 917		}
 918
 919		// reset all ftype hooks
 920		foreach($this->_get_ftypes() as $class_name => $ftype)
 921		{
 922			$this->_insert_ftype_hooks($ftype);
 923		}
 924	}
 925
 926	/**
 927	 * Settings Form
 928	 *
 929	 * @param array  $current  Current extension settings (not site-specific)
 930	 * @see   http://expressionengine.com/docs/development/extensions.html#settings
 931	 */
 932	function settings_form()
 933	{
 934		global $DB, $DSP, $LANG, $IN, $SD, $PREFS;
 935
 936		// Breadcrumbs
 937		$DSP->crumbline = TRUE;
 938		$DSP->title = $LANG->line('extension_settings');
 939		$DSP->crumb = $DSP->anchor(BASE.AMP.'C=admin'.AMP.'area=utilities', $LANG->line('utilities'))
 940		            . $DSP->crumb_item($DSP->anchor(BASE.AMP.'C=admin'.AMP.'M=utilities'.AMP.'P=extensions_manager', $LANG->line('extensions_manager')))
 941		            . $DSP->crumb_item(FF_NAME);
 942		$DSP->right_crumb($LANG->line('disable_extension'), BASE.AMP.'C=admin'.AMP.'M=utilities'.AMP.'P=toggle_extension_confirm'.AMP.'which=disable'.AMP.'name='.$IN->GBL('name'));
 943
 944		// Donations button
 945	    $DSP->body .= '<div class="donations">'
 946	                . '<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2181794" target="_blank">'
 947	                . $LANG->line('donate')
 948	                . '</a>'
 949	                . '</div>';
 950
 951		// open form
 952		$DSP->body .= '<h1>'.FF_NAME.' <small>'.FF_VERSION.'</small></h1>'
 953		            . $DSP->form_open(
 954		                  array(
 955		                    'action' => 'C=admin'.AMP.'M=utilities'.AMP.'P=save_extension_settings',
 956		                    'name'   => 'settings_subtext',
 957		                    'id'     => 'ffsettings'
 958		                  ),
 959		                  array(
 960		                    'name' => strtolower(FF_CLASS)
 961		                  ));
 962
 963		// initialize Fieldframe_SettingsDisplay
 964		$SD = new Fieldframe_SettingsDisplay();
 965
 966		// import lang files
 967		$LANG->fetch_language_file('publish_ad');
 968
 969		// fieldtypes folder
 970		$DSP->body .= $SD->block('fieldtypes_folder_title')
 971		            . $SD->info_row('fieldtypes_folder_info')
 972		            . $SD->row(array(
 973		                         $SD->label('fieldtypes_path_label', 'fieldtypes_path_subtext'),
 974		                         $SD->text('fieldtypes_path',
 975		                                       (isset($PREFS->core_ini['ft_path']) ? $PREFS->core_ini['ft_path'] : $this->settings['fieldtypes_path']),
 976		                                       array('extras' => (isset($PREFS->core_ini['ft_path']) ? ' disabled="disabled" ' : '')))
 977		                       ))
 978		            . $SD->row(array(
 979		                         $SD->label('fieldtypes_url_label', 'fieldtypes_url_subtext'),
 980		                         $SD->text('fieldtypes_url',
 981		                                       (isset($PREFS->core_ini['ft_url']) ? $PREFS->core_ini['ft_url'] : $this->settings['fieldtypes_url']),
 982		                                       array('extras' => (isset($PREFS->core_ini['ft_url']) ? ' disabled="disabled" ' : '')))
 983		                       ))
 984		            . $SD->block_c();
 985
 986		// Check for Updates
 987		$lgau_query = $DB->query("SELECT class FROM exp_extensions
 988		                            WHERE class = 'Lg_addon_updater_ext' AND enabled = 'y' LIMIT 1");
 989		$lgau_enabled = $lgau_query->num_rows ? TRUE : FALSE;
 990		$DSP->body .= $SD->block('check_for_updates_title')
 991		            . $SD->info_row('check_for_updates_info')
 992		            . $SD->row(array(
 993		                           $SD->label('check_for_updates_label', 'check_for_updates_subtext'),
 994		                           $SD->radio_group('check_for_updates', (($this->settings['check_for_updates'] != 'n') ? 'y' : 'n'), array('y'=>'yes', 'n'=>'no'))
 995		                         ))
 996		            . $SD->block_c();
 997
 998		// Fieldtypes Manager
 999		$this->fieldtypes_manager(FALSE, $SD);
1000
1001		// Close form
1002		$DSP->body .= $DSP->qdiv('itemWrapperTop', $DSP->input_submit())
1003		            . $DSP->form_c();
1004
1005
1006		ob_start();
1007?>
1008<style type="text/css" charset="utf-8">
1009  .donations { float:right; }
1010  .donations a { display:block; margin:-2px 10px 0 0; padding:5px 0 5px 67px; width:193px; height:15px; font-size:12px; line-height:15px; background:url(http://brandon-kelly.com/images/shared/donations.png) no-repeat 0 0; color:#000; font-weight:bold; }
1011  h1 { padding:7px 0; }
1012</style>
1013<?php
1014		$this->snippets['head'][] = ob_get_contents();
1015		ob_end_clean();
1016	}
1017
1018	/**
1019	 * Add Slash to URL/Path
1020	 *
1021	 * @param  string  $path  The user-submitted path
1022	 * @return string  $path with a slash at the end
1023	 * @access private
1024	 */
1025	function _add_slash($path)
1026	{
1027		if (substr($path, -1) != '/')
1028		{
1029			$path .= '/';
1030		}
1031		return $path;
1032	}
1033
1034	/**
1035	 * Save Settings
1036	 */
1037	function save_settings()
1038	{
1039		global $DB;
1040
1041		// save new FF site settings
1042		$this->settings = array(
1043			'fieldtypes_url'    => ((isset($_POST['fieldtypes_url']) AND $_POST['fieldtypes_url']) ? $this->_add_slash($_POST['fieldtypes_url']) : ''),
1044			'fieldtypes_path'   => ((isset($_POST['fieldtypes_path']) AND $_POST['fieldtypes_path']) ? $this->_add_slash($_POST['fieldtypes_path']) : ''),
1045			'check_for_updates' => ($_POST['check_for_updates'] != 'n' ? 'y' : 'n')
1046		);
1047		$DB->query($DB->update_string('exp_extensions', array('settings' => $this->_serialize($this->settings)), 'class = "'.FF_CLASS.'"'));
1048
1049		// set the FT_PATH and FT_URL constants
1050		$this->_define_constants();
1051
1052		// save Fieldtypes Manager data
1053		$this->save_fieldtypes_manager();
1054	}
1055
1056	/**
1057	 * Fieldtypes Manager
1058	 */
1059	function fieldtypes_manager($standalone=TRUE, $SD=NULL)
1060	{
1061		global $DB, $DSP, $LANG, $IN, $PREFS, $SD;
1062
1063		// are they allowed to access this?
1064		if (! $DSP->allowed_group('can_admin_utilities'))
1065		{
1066			return $DSP->no_access_message();
1067		}
1068
1069		if ( ! $SD)
1070		{
1071			// initialize Fieldframe_SettingsDisplay
1072			$SD = new Fieldframe_SettingsDisplay();
1073		}
1074
1075		if ($standalone)
1076		{
1077			// save submitted settings
1078			if ($this->save_fieldtypes_manager())
1079			{
1080				$DSP->body .= $DSP->qdiv('box', $DSP->qdiv('success', $LANG->line('settings_update')));
1081			}
1082
1083			// load language file
1084			$LANG->fetch_language_file('fieldframe');
1085
1086			// Breadcrumbs
1087			$DSP->crumbline = TRUE;
1088			$DSP->title = $LANG->line('fieldtypes_manager');
1089			$DSP->crumb = $DSP->anchor(BASE.AMP.'C=admin'.AMP.'area=utilities', $LANG->line('utilities'))
1090			            . $DSP->crumb_item($LANG->line('fieldtypes_manager'));
1091
1092			// open form
1093			$DSP->body .= $DSP->form_open(
1094			                  array(
1095			                    'action' => 'C=admin'.AMP.'M=utilities'.AMP.'P=fieldtypes_manager',
1096			                    'name'   => 'settings_subtext',
1097			                    'id'     => 'ffsettings'
1098			                  ),
1099			                  array(
1100			                    'name' => strtolower(FF_CLASS)
1101			                  ));
1102		}
1103
1104		// fieldtype settings
1105		$DSP->body .= $SD->block('fieldtypes_manager', 5);
1106
1107		// initialize fieldtypes
1108		if ($ftypes = $this->_get_all_installed_ftypes())
1109		{
1110			// add the headers
1111			$DSP->body .= $SD->heading_row(array(
1112			                                   $LANG->line('fieldtype'),
1113			                                   $LANG->line('fieldtype_enabled'),
1114			                                   $LANG->line('settings'),
1115			                                   $LANG->line('documentation')
1116			                                 ));
1117
1118			$row_ids = array();
1119
1120			foreach($ftypes as $class_name => $ftype)
1121			{
1122				$row_id = 'ft_'.$ftype->_class_name;
1123				$row_ids[] = '"'.$row_id.'"';
1124
1125				if (method_exists($ftype, 'display_site_settings'))
1126				{
1127					if ( ! $ftype->info['no_lang']) $LANG->fetch_language_file($class_name);
1128					$site_settings = $ftype->display_site_settings();
1129				}
1130				else
1131				{
1132					$site_settings = FALSE;
1133				}
1134
1135				$DSP->body .= $SD->row(array(
1136				                         $SD->label($ftype->info['name'].NBS.$DSP->qspan('xhtmlWrapperLight defaultSmall', $ftype->info['version']), $ftype->info['desc']),
1137				                         ($ftype->_requires
1138				                            ?  '--'
1139				                            :  $SD->radio_group('ftypes['.$class_name.'][enabled]', ($ftype->_is_enabled ? 'y' : 'n'), array('y'=>'yes', 'n'=>'no'))),
1140				                         ($site_settings
1141				                            ?  '<a class="toggle show" id="'.$row_id.'_show" href="#ft_'.$class_name.'_settings"><img src="'.$PREFS->ini('theme_folder_url', 1).'cp_global_images/expand.gif" border="0">  '.$LANG->line('show').'</a>'
1142				                               . '<a class="toggle hide" id="'.$row_id.'_hide"><img src="'.$PREFS->ini('theme_folder_url', 1).'cp_global_images/collapse.gif" border="0">  '.$LANG->line('hide').'</a>'
1143				                            :  '--'),
1144				                         ($ftype->info['docs_url'] ? '<a href="'.stripslashes($ftype->info['docs_url']).'">'.$LANG->line('documentation').'</a>' : '--')
1145				                       ), NULL, array('id' => $row_id));
1146
1147				if ($ftype->_requires)
1148				{
1149					$data = '<p>'.$ftype->info['name'].' '.$LANG->line('requires').':</p>'
1150					      . '<ul>';
1151					foreach($ftype->_requires as $dependency => $version)
1152					{
1153						$data .= '<li class="default">'.$dependency.' '.$version.' '.$LANG->line('or_later').'</li>';
1154					}
1155					$data .= '</ul>';
1156					$DSP->body .= $SD->row(array('', $data), $SD->row_class);
1157				}
1158				else if ($site_settings)
1159				{
1160					$data = '<div class="ftsettings">'
1161					      .   $this->_group_ftype_inputs($ftype->_class_name, $site_settings)
1162					      . $DSP->div_c();
1163					$DSP->body .= $SD->row(array($data), '', array('id' => $row_id.'_settings', 'style' => 'display:none;'));
1164				}
1165			}
1166
1167			ob_start();
1168?>
1169<script type="text/javascript" charset="utf-8">
1170	;var urlParts = document.location.href.split('#'),
1171		anchor = urlParts[1];
1172	function ffEnable(ft) {
1173		ft.show.className = "toggle show";
1174		ft.show.onclick = function() {
1175			ft.show.style.display = "none";
1176			ft.hide.style.display = "block";
1177			ft.settings.style.display = "table-row";
1178		};
1179		ft.hide.onclick = function() {
1180			ft.show.style.display = "block";
1181			ft.hide.style.display = "none";
1182			ft.settings.style.display = "none";
1183		};
1184	}
1185	function ffDisable(ft) {
1186		ft.show.className = "toggle show disabled";
1187		ft.show.onclick = null;
1188		ft.show.style.display = "block";
1189		ft.hide.style.display = "none";
1190		ft.settings.style.display = "none";
1191	}
1192	function ffInitRow(rowId) {
1193		var ft = {
1194			tr: document.getElementById(rowId),
1195			show: document.getElementById(rowId+"_show"),
1196			hide: document.getElementById(rowId+"_hide"),
1197			settings: document.getElementById(rowId+"_settings")
1198		};
1199		if (ft.settings) {
1200			ft.toggles = ft.tr.getElementsByTagName("input");
1201			ft.toggles[0].onchange = function() { ffEnable(ft); };
1202			ft.toggles[1].onchange = function() { ffDisable(ft); };
1203			if (ft.toggles[0].checked) ffEnable(ft);
1204			else ffDisable(ft);
1205			if (anchor == rowId+'_settings') {
1206				ft.show.onclick();
1207			}
1208		}
1209	}
1210	var ffRowIds = [<?php echo implode(',', $row_ids) ?>];
1211	for (var i = 0; i < ffRowIds.length; i++) {
1212		ffInitRow(ffRowIds[i]);
1213	}
1214</script>
1215<?php
1216			$this->snippets['body'][] = ob_get_contents();
1217			ob_end_clean();
1218		}
1219		else if ( ! defined('FT_PATH'))
1220		{
1221			$DSP->body .= $SD->info_row('no_fieldtypes_path');
1222		}
1223		else if (in_array('bad_ft_path', $this->errors))
1224		{
1225			$DSP->body .= $SD->info_row('bad_fieldtypes_path');
1226		}
1227		else
1228		{
1229			$DSP->body .= $SD->info_row('no_fieldtypes');
1230		}
1231
1232		$DSP->body .= $SD->block_c();
1233
1234		if ($standalone)
1235		{
1236			// Close form
1237			$DSP->body .= $DSP->qdiv('itemWrapperTop', $DSP->input_submit($LANG->line('apply')))
1238			            . $DSP->form_c();
1239		}
1240
1241		ob_start();
1242?>
1243<style type="text/css" charset="utf-8">
1244	#ffsettings a.toggle { display:block; cursor:pointer; }
1245	#ffsettings a.toggle.hide { display:none; }
1246	#ffsettings a.toggle.disabled { color:#000; opacity:0.4; cursor:default; }
1247	#ffsettings .ftsettings { margin:-3px -1px -1px; }
1248	#ffsettings .ftsettings, #ffsettings .ftsettings .tableCellOne, #ffsettings .ftsettings .tableCellTwo, #ffsettings .ftsettings .box { background:#262e33; color:#999; }
1249	#ffsettings .ftsettings .box { margin: 0; padding: 10px 15px; border: none; border-top: 1px solid #283036; background: -webkit-gradient(linear, 0 0, 0 100%, from(#262e33), to(#22292e)); }
1250	#ffsettings .ftsettings table tr td .box { margin: -1px -8px; }
1251	#ffsettings .ftsettings a, #ffsettings .ftsettings p, #ffsettings .ftsettings .subtext { color: #999; }
1252	#ffsettings .ftsettings input.input, #ffsettings .ftsettings textarea { background:#fff; color:#333; }
1253	#ffsettings .ftsettings table { border:none; }
1254	#ffsettings .ftsettings table tr td { border-top:1px solid #1d2326; padding-left:8px; padding-right:8px;  }
1255	#ffsettings .ftsettings table tr td.tableHeading { color:#ddd; background:#232a2e; }
1256	#ffsettings .ftsettings .defaultBold { color:#ccc; }
1257	#ffsettings .ftsettings .tableCellOne, #ffsettings .ftsettings .tableCellOneBold, #ffsettings .ftsettings .tableCellTwo, #ffsettings .ftsettings .tableCellTwoBold { border-bottom:none; }
1258</style>
1259<?php
1260		$this->snippets['head'][] = ob_get_contents();
1261		ob_end_clean();
1262	}
1263
1264	/**
1265	 * Save Fieldtypes Manager Settings
1266	 */
1267	function save_fieldtypes_manager()
1268	{
1269		global $DB;
1270
1271		// fieldtype settings
1272		if (isset($_POST['ftypes']))
1273		{
1274			foreach($_POST['ftypes'] as $file => $ftype_post)
1275			{
1276				// Initialize
1277				if (($ftype = $this->_init_ftype($file)) !== FALSE)
1278				{
1279					$enabled = ($ftype_post['enabled'] == 'y');
1280
1281					// skip if new and not enabled yet
1282					if ( ! $enabled AND $ftype->_is_new) continue;
1283
1284					// insert a new row if it's new
1285					if ($enabled AND $ftype->_is_new)
1286					{
1287						$DB->query($DB->insert_string('exp_ff_fieldtypes', array(
1288							'class'   => $file,
1289							'version' => $ftype->info['version']
1290						)));
1291
1292						// get the fieldtype_id
1293						$query = $DB->query('SELECT fieldtype_id FROM exp_ff_fieldtypes WHERE class = "'.$file.'"');
1294						$ftype->_fieldtype_id = $query->row['fieldtype_id'];
1295
1296						// insert hooks
1297						$this->_insert_ftype_hooks($ftype);
1298
1299						// call update()
1300						if (method_exists($ftype, 'update'))
1301						{
1302							$ftype->update(FALSE);
1303						}
1304					}
1305
1306					$data = array(
1307						'enabled' => ($enabled ? 'y' : 'n')
1308					);
1309
1310					if ($enabled)
1311					{
1312						$settings = (isset($_POST['ftype']) AND isset($_POST['ftype'][$ftype->_class_name]))
1313						  ?  $_POST['ftype'][$ftype->_class_name]
1314						  :  array();
1315
1316						// let the fieldtype do what it wants with them
1317						if (method_exists($ftype, 'save_site_settings'))
1318						{
1319							$settings = $ftype->save_site_settings($settings);
1320							if ( ! is_array($settings)) $settings = array();
1321						}
1322						$data['settings'] = $this->_serialize($settings);
1323					}
1324
1325					$DB->query($DB->update_string('exp_ff_fieldtypes', $data, 'fieldtype_id = "'.$ftype->_fieldtype_id.'"'));
1326				}
1327			}
1328
1329			return TRUE;
1330		}
1331
1332		return FALSE;
1333	}
1334
1335	/**
1336	 * Get Last Call
1337	 *
1338	 * @param  mixed  $param  Parameter sent by extension hook
1339	 * @return mixed  Return value of last extension call if any, or $param
1340	 * @access private
1341	 */
1342	function get_last_call($param=FALSE)
1343	{
1344		global $EXT;
1345		return isset($this->_last_call)
1346		  ?  $this->_last_call
1347		  :  ($EXT->last_call !== FALSE ? $EXT->last_call : $param);
1348	}
1349
1350	/**
1351	 * Forward hook to fieldtype
1352	 */
1353	function forward_hook($hook, $priority, $args=array())
1354	{
1355		$ftype_hooks = $this->_get_ftype_hooks();
1356		if (isset($ftype_hooks[$hook]) AND isset($ftype_hooks[$hook][$priority]))
1357		{
1358			$ftypes = $this->_get_ftypes();
1359
1360			foreach ($ftype_hooks[$hook][$priority] as $class_name => $method)
1361			{
1362				if (isset($ftypes[$class_name]) AND method_exists($ftypes[$class_name], $method))
1363				{
1364					$this->_last_call = call_user_func_array(array(&$ftypes[$class_name], $method), $args);
1365				}
1366			}
1367		}
1368		if (isset($this->_last_call))
1369		{
1370			$r = $this->_last_call;
1371			unset($this->_last_call);
1372		}
1373		else
1374		{
1375			$r = $this->get_last_call();
1376		}
1377		return $r;
1378	}
1379
1380	function forward_ff_hook($hook, $args=array(), $r=TRUE)
1381	{
1382		$this->_last_call = $r;
1383		//           ------------ These braces brought to you by Leevi Graham ------------
1384		//          ↓                                                                     ↓
1385		$priority = (isset($this->hooks[$hook]) AND isset($this->hooks[$hook]['priority']))
1386		  ?  $this->hooks[$hook]['priority']
1387		  :  10;
1388		return $this->forward_hook($hook, $priority, $args);
1389	}
1390
1391	/**
1392	 * Get Line
1393	 *
1394	 * @param  string  $line  unlocalized string or the name of a $LANG line
1395	 * @return string  Localized string
1396	 */
1397	function get_line($line)
1398	{
1399		global $LANG;
1400		$loc_line = $LANG->line($line);
1401		return $loc_line ? $loc_line : $line;
1402	}
1403
1404	/**
1405	 * Sessions Start
1406	 *
1407	 * - Reset any session class variable
1408	 * - Override the whole session check
1409	 * - Modify default/guest settings
1410	 *
1411	 * @param object  $this  The current instantiated Session class with all of its variables and functions,
1412	 *                       use a reference in your functions to modify.
1413	 * @see   http://expressionengine.com/developers/extension_hooks/sessions_start/
1414	 */
1415	function sessions_start($sess)
1416	{
1417		global $IN;
1418
1419		// are we saving a field?
1420		if($IN->GBL('M', 'GET') == 'blog_admin' AND $IN->GBL('P', 'GET') == 'update_weblog_fields' AND isset($_POST['field_type']))
1421		{
1422			$this->publish_admin_edit_field_save();
1423		}
1424
1425		$args = func_get_args();
1426		return $this->forward_ff_hook('sessions_start', $args);
1427	}
1428
1429	/**
1430	 * Publish Admin - Edit Field Form - Fieldtype Menu
1431	 *
1432	 * Allows modifying or adding onto Custom Weblog Fieldtype Pulldown
1433	 *
1434	 * @param  array   $data  The data about this field from the database
1435	 * @param  string  $typemenu  The contents of the type menu
1436	 * @return string  The modified $typemenu
1437	 * @see    http://expressionengine.com/developers/extension_hooks/publish_admin_edit_field_type_pulldown/
1438	 */
1439	function publish_admin_edit_field_type_pulldown($data, $typemenu)
1440	{
1441		global $DSP;
1442
1443		$r = $this->get_last_call($typemenu);
1444
1445		foreach($this->_get_ftypes() as $class_name => $ftype)
1446		{
1447			// only list normal fieldtypes
1448			if (method_exists($ftype, 'display_field'))
1449			{
1450				$field_type = 'ftype_id_'.$ftype->_fieldtype_id;
1451				$r .= $DSP->input_select_option($field_type, $ftype->info['name'], ($data['field_type'] == $field_type ? 1 : 0));
1452			}
1453		}
1454
1455		$args = func_get_args();
1456		return $this->forward_ff_hook('publish_admin_edit_field_type_pulldown', $args, $r);
1457	}
1458
1459	/**
1460	 * Publish Admin - Edit Field Form - Javascript
1461	 *
1462	 * Allows modifying or adding onto Custom Weblog Field JS
1463	 *
1464	 * @param  array   $data  The data about this field from the database
1465	 * @param  string  $js    Currently existing javascript
1466	 * @return string  The modified $js
1467	 * @see    http://expressionengine.com/developers/extension_hooks/publish_admin_edit_field_js/
1468	 */
1469	function publish_admin_edit_field_js($data, $js)
1470	{
1471		global $LANG, $REGX;
1472
1473		// Fetch the FF lang file
1474		$LANG->fetch_language_file('fieldframe');
1475
1476		// Prepare fieldtypes for following Publish Admin hooks
1477		$field_settings_tmpl = array(
1478			'cell1' => '',
1479			'cell2' => '',
1480			'rows' => array(),
1481			'formatting_available' => FALSE,
1482			'direction_available' => FALSE
1483		);
1484
1485		$formatting_available = array();
1486		$direction_available = array();
1487		$prev_ftype_id = '';
1488
1489		$this->data = $data;
1490
1491		foreach($this->_get_ftypes() as $class_name => $ftype)
1492		{
1493			$ftype_id = 'ftype_id_'.$ftype->_fieldtype_id;
1494			$selected = ($ftype_id == $this->data['field_type']) ? TRUE : FALSE;
1495			if (method_exists($ftype, 'display_field_settings'))
1496			{
1497				// Load the language file
1498				if ( ! $ftype->info['no_lang']) $LANG->fetch_language_file($class_name);
1499
1500				$field_settings = array_merge(
1501					(isset($ftype->default_field_settings) ? $ftype->default_field_settings : array()),
1502					($selected ? $this->_unserialize($this->data['ff_settings']) : array())
1503				);
1504				$ftype->_field_settings = array_merge($field_settings_tmpl, $ftype->display_field_settings($field_settings));
1505			}
1506			else
1507			{
1508				$ftype->_field_settings = $field_settings_tmpl;
1509			}
1510
1511			if ($ftype->_field_settings['formatting_available']) $formatting_available[] = $ftype->_fieldtype_id;
1512			if ($ftype->_field_settings['direction_available']) $direction_available[] = $ftype->_fieldtype_id;
1513
1514			if ($selected) $prev_ftype_id = $ftype_id;
1515		}
1516
1517		unset($this->data);
1518
1519		// Add the JS
1520		ob_start();
1521?>
1522var prev_ftype_id = '<?php echo $prev_ftype_id ?>';
1523
1524	$1
1525		if (prev_ftype_id)
1526		{
1527			var c=1, r=1;
1528			while(cell = document.getElementById(prev_ftype_id+'_cell'+c))
1529			{
1530				cell.style.display = 'none';
1531				c++;
1532			}
1533			while(row = document.getElementById(prev_ftype_id+'_row'+r))
1534			{
1535				row.style.display = 'none';
1536				r++;
1537			}
1538		}
1539
1540		if (id.match(/^ftype_id_\d+$/))
1541		{
1542			var c=1, r=1;
1543
1544			// show cells
1545			while(cell = document.getElementById(id+'_cell'+c))
1546			{
1547				//var showDiv = document.getElementById(id+'_cell'+c);
1548				var divs = cell.parentNode.childNodes;
1549				for(var i=0; i < divs.length; i++)
1550				{
1551					var div = divs[i];
1552					if ( ! (div.nodeType == 1 && div.id)) continue;
1553					div.style.display = (div == cell) ? 'block' : 'none';
1554				}
1555				c++;
1556			}
1557
1558			// show rows
1559			while(row = document.getElementById(id+'_row'+r))
1560			{
1561				row.style.display = 'table-row';
1562				r++;
1563			}
1564
1565			// show/hide formatting
1566			if ([<?php echo implode(',', $formatting_available) ?>].indexOf(parseInt(id.substring(9))) != -1)
1567			{
1568				document.getElementById('formatting_block').style.display = 'block';
1569				document.getElementById('formatting_unavailable').style.display = 'none';
1570			}
1571			else
1572			{
1573				document.getElementById('formatting_block').style.display = 'none';
1574				document.getElementById('formatting_unavailable').style.display = 'block';
1575			}
1576
1577			// show/hide direction
1578			if ([<?php echo implode(',', $direction_available) ?>].indexOf(parseInt(id.substring(9))) != -1)
1579			{
1580				document.getElementById('direction_available').style.display = 'block';
1581				document.getElementById('direction_unavailable').style.display = 'none';
1582			}
1583			else
1584			{
1585				document.getElementById('direction_available').style.display = 'none';
1586				document.getElementById('direction_unavailable').style.display = 'block';
1587			}
1588
1589			prev_ftype_id = id;
1590		}
1591<?php
1592		$r = $this->get_last_call($js);
1593		$r = preg_replace('/(function\s+showhide_element\(\s*id\s*\)\s*{)/is', ob_get_contents(), $r);
1594		ob_end_clean();
1595
1596		$args = func_get_args();
1597		return $this->forward_ff_hook('publish_admin_edit_field_js', $args, $r);
1598	}
1599
1600	/**
1601	 * Publish Admin - Edit Field Form - Cell
1602	 *
1603	 * @param  array   $data   The data about this field from the database
1604	 * @param  string  $cell   The contents of the cell
1605	 * @param  string  $index  The cell index
1606	 * @return string  The modified $cell
1607	 * @access private
1608	 */
1609	function _publish_admin_edit_field_type_cell($data, $cell, $index)
1610	{
1611		$r = $this->get_last_call($cell);
1612
1613		foreach($this->_get_ftypes() as $class_name => $ftype)
1614		{
1615			$ftype_id = 'ftype_id_'.$ftype->_fieldtype_id;
1616			$selected = ($data['field_type'] == $ftype_id);
1617
1618			$field_settings = $this->_group_ftype_inputs($ftype_id, $ftype->_field_settings['cell'.$index]);
1619
1620			$r .= '<div id="'.$ftype_id.'_cell'.$index.'" style="margin-top:5px; display:'.($selected ? 'block' : 'none').';">'
1621			    . $field_settings
1622			    . '</div>';
1623		}
1624
1625		return $r;
1626	}
1627
1628	/**
1629	 * Publish Admin - Edit Field Form - Cell One
1630	 *
1631	 * Allows modifying or adding onto Custom Weblog Fieldtype - First Table Cell
1632	 *
1633	 * @param  array   $data  The data about this field from the database
1634	 * @param  string  $cell  The contents of the cell
1635	 * @return string  The modified $cell
1636	 * @see    http://expressionengine.com/developers/extension_hooks/publish_admin_edit_field_type_cellone/
1637	 */
1638	function publish_admin_edit_field_type_cellone($data, $cell)
1639	{
1640		global $DSP;
1641
1642		$r = $this->_publish_admin_edit_field_type_cell($data, $cell, '1');
1643
1644		// formatting
1645		foreach($this->_get_ftypes() as $class_name => $ftype)
1646		{
1647			$ftype_id = 'ftype_id_'.$ftype->_fieldtype_id;
1648			$r .= $DSP->input_hidden('ftype['.$ftype_id.'][formatting_available]', ($ftype->_field_settings['formatting_available'] ? 'y' : 'n'));
1649		}
1650
1651		$args = func_get_args();
1652		return $this->forward_ff_hook('publish_admin_edit_field_type_cellone', $args, $r);
1653	}
1654
1655	/**
1656	 * Publish Admin - Edit Field Form - Cell Two
1657	 *
1658	 * Allows modifying or adding onto Custom Weblog Fieldtype - Second Table Cell
1659	 *
1660	 * @param  array   $data  The data about this field from the database
1661	 * @param  string  $cell  The contents of the cell
1662	 * @return string  The modified $cell
1663	 * @see    http://expressionengine.com/developers/extension_hooks/publish_admin_edit_field_type_celltwo/
1664	 */
1665	function publish_admin_edit_field_type_celltwo($data, $cell)
1666	{
1667		$r = $this->_publish_admin_edit_field_type_cell($data, $cell, '2');
1668		$args = func_get_args();
1669		return $this->forward_ff_hook('publish_admin_edit_field_type_celltwo', $args, $r);
1670	}
1671
1672	/**
1673	 * Publish Admin - Edit Field Form - Format
1674	 *
1675	 * Allows modifying or adding onto Default Text Formatting Cell
1676	 *
1677	 * @param  array   $data  The data about this field from the database
1678	 * @param  string  $y     The current contents of the format cell
1679	 * @return string  The modified $y
1680	 * @see    http://expressionengine.com/developers/extension_hooks/publish_admin_edit_field_format/
1681	 */
1682	function publish_admin_edit_field_format($data, $y)
1683	{
1684		$y = $this->get_last_call($y);
1685		$args = func_get_args();
1686		return $this->forward_ff_hook('publish_admin_edit_field_format', $args, $y);
1687	}
1688
1689	/**
1690	 * Publish Admin - Edit Field Form - Extra Row
1691	 *
1692	 * Allows modifying or adding onto the Custom Field settings table
1693	 *
1694	 * @param  array   $data  The data about this field from the database
1695	 * @param  string  $r     The current contents of the page
1696	 * @return string  The modified $r
1697	 * @see    http://expressionengine.com/developers/extension_hooks/publish_admin_edit_field_extra_row/
1698	 */
1699	function publish_admin_edit_field_extra_row($data, $r)
1700	{
1701		global $DSP, $LANG;
1702
1703		$r = $this->get_last_call($r);
1704
1705		if ($ftypes = $this->_get_ftypes())
1706		{
1707			$rows = '';
1708			foreach($ftypes as $class_name => $ftype)
1709			{
1710				$ftype_id = 'ftype_id_'.$ftype->_fieldtype_id;
1711				$selected = ($data['field_type'] == $ftype_id);
1712
1713				foreach($ftype->_field_settings['rows'] as $index => $row)
1714				{
1715					$class = $index % 2 ? 'tableCellOne' : 'tableCellTwo';
1716					$rows .= '<tr id="'.$ftype_id.'_row'.($index+1).'"' . ($selected ? '' : ' style="display:none;"') . '>'
1717					       . '<td class="'.$class.'"'.(isset($row[1]) ? '' : ' colspan="2"').'>'
1718					       . $this->_group_ftype_inputs($ftype_id, $row[0])
1719					       . $DSP->td_c()
1720					       . (isset($row[1])
1721					            ?  $DSP->td($class)
1722					             . $this->_group_ftype_inputs($ftype_id, $row[1])
1723					             . $DSP->td_c()
1724					             . $DSP->tr_c()
1725					            : '');
1726				}
1727
1728				if ($selected)
1729				{
1730					// show/hide formatting
1731					if ($ftype->_field_settings['formatting_available'])
1732					{
1733						$formatting_search = 'none';
1734						$formatting_replace = 'block';
1735					}
1736					else
1737					{
1738						$formatting_search = 'block';
1739						$formatting_replace = 'none';
1740					}
1741					$r = preg_replace('/(\sid\s*=\s*([\'\"])formatting_block\2.*display\s*:\s*)'.$formatting_search.'(\s*;)/isU', '$1'.$formatting_replace.'$3', $r);
1742					$r = preg_replace('/(\sid\s*=\s*([\'\"])formatting_unavailable\2.*display\s*:\s*)'.$formatting_replace.'(\s*;)/isU', '$1'.$formatting_search.'$3', $r);
1743
1744					// show/hide direction
1745					if ($ftype->_field_settings['direction_available'])
1746					{
1747						$direction_search = 'none';
1748						$direction_replace = 'block';
1749					}
1750					else
1751					{
1752						$direction_search = 'block';
1753						$direction_replace = 'none';
1754					}
1755					$r = preg_replace('/(\sid\s*=\s*([\'\"])direction_available\2.*display\s*:\s*)'.$direction_search.'(\s*;)/isU', '$1'.$direction_replace.'$3', $r);
1756					$r = preg_replace('/(\sid\s*=\s*([\'\"])direction_unavailable\2.*display\s*:\s*)'.$direction_replace.'(\s*;)/isU', '$1'.$direction_search.'$3', $r);
1757				}
1758			}
1759
1760			$r = preg_replace('/(<tr>\s*<td[^>]*>\s*<div[^>]*>\s*'.$LANG->line('deft_field_formatting').'\s*<\/div>)/is', $rows.'$1', $r);
1761		}
1762
1763		$args = func_get_args();
1764		return $this->forward_ff_hook('publish_admin_edit_field_extra_row', $args, $r);
1765	}
1766
1767	/**
1768	 * Publish Admin - Edit Field Form - Save Field
1769	 *
1770	 * Made-up hook called by sessions_start
1771	 * when $_POST['field_type'] is set
1772	 */
1773	function publish_admin_edit_field_save()
1774	{
1775		global $DB;
1776
1777		// is this a FF fieldtype?
1778		if (preg_match('/^ftype_id_(\d+)$/', $_POST['field_type'], $matches))
1779		{
1780			if (isset($matches[1]))
1781			{
1782				$ftype_id = $matches[1];
1783				$settings = (isset($_POST['ftype']) AND isset($_POST['ftype'][$_POST['field_type']]))
1784				  ?  $_POST['ftype'][$_POST['field_type']]
1785				  :  array();
1786
1787				// formatting
1788				if (isset($settings['formatting_available']))
1789				{
1790					if ($settings['formatting_available'] == 'n')
1791					{
1792						$_POST['field_fmt'] = 'none';
1793						$_POST['field_show_fmt'] = 'n';
1794					}
1795					unset($settings['formatting_available']);
1796				}
1797
1798				// initialize the fieldtype
1799				$query = $DB->query('SELECT * FROM exp_ff_fieldtypes WHERE fieldtype_id = "'.$ftype_id.'"');
1800				if ($query->row)
1801				{
1802					// let the fieldtype modify the settings
1803					if (($ftype = $this->_init_ftype($query->row)) !== FALSE)
1804					{
1805						if (method_exists($ftype, 'save_field_settings'))
1806						{
1807							$settings = $ftype->save_field_settings($settings);
1808							if ( ! is_array($settings)) $settings = array();
1809						}
1810					}
1811				}
1812
1813				// save settings as a post var
1814				$_POST['ff_settings'] = $this->_serialize($settings);
1815			}
1816		}
1817
1818		// unset extra FF post vars
1819		foreach($_POST as $key => $value)
1820		{
1821			if (substr($key, 0, 5) == 'ftype')
1822			{
1823				unset($_POST[$key]);
1824			}
1825		}
1826	}
1827
1828	/**
1829	 * Display - Show Full Control Panel - Start
1830	 *
1831	 * - Rewrite CP's HTML
1832	 * - Find/Replace stuff, etc.
1833	 *
1834	 * @param  string  $end  The content of the admin page to be outputted
1835	 * @return string  The modified $out
1836	 * @see    http://expressionengine.com/developers/extension_hooks/show_full_control_panel_end/
1837	 */
1838	function show_full_control_panel_start($out = '')
1839	{
1840		global $IN, $DB, $REGX, $DSP;
1841
1842		$out = $this->get_last_call($out);
1843
1844		// are we displaying the custom field list?
1845		if ($IN->GBL('C', 'GET') == 'admin' AND $IN->GBL('M', 'GET') == 'utilities' AND $IN->GBL('P', 'GET') == 'fieldtypes_manager' AND defined('FT_PATH'))
1846		{
1847			$this->fieldtypes_manager();
1848		}
1849
1850		$args = func_get_args();
1851		return $this->forward_ff_hook('show_full_control_panel_start', $args, $out);
1852	}
1853
1854	/**
1855	 * Display - Show Full Control Panel - End
1856	 *
1857	 * - Rewrite CP's HTML
1858	 * - Find/Replace stuff, etc.
1859	 *
1860	 * @param  string  $end  The content of the admin page to be outputted
1861	 * @return string  The modified $out
1862	 * @see    http://expressionengine.com/developers/extension_hooks/show_full_control_panel_end/
1863	 */
1864	function show_full_control_panel_end($out = '')
1865	{
1866		global $IN, $DB, $REGX, $DSP, $LANG;
1867
1868		$out = $this->get_last_call($out);
1869
1870		// are we displaying the custom field list?
1871		if ($IN->GBL('M', 'GET') == 'blog_admin' AND in_array($IN->GBL('P', 'GET'), array('field_editor', 'update_weblog_fields', 'delete_field', 'update_field_order')))
1872		{
1873			// get the FF fieldtypes
1874			foreach($this->_get_fields() as $field_id => $field)
1875			{
1876				// add fieldtype name to this field
1877				$out = preg_replace("/(C=admin&amp;M=blog_admin&amp;P=edit_field&amp;field_id={$field_id}[\'\"].*?<\/td>.*?<td.*?>.*?<\/td>.*?)<\/td>/is",
1878				                      '$1'.$REGX->form_prep($field['ftype']->info['name']).'</td>', $out);
1879			}
1880		}
1881		// is this the main admin page?
1882		else if ($IN->GBL('C', 'GET') == 'admin' AND ! ($IN->GBL('M', 'GET') AND $IN->GBL('P', 'GET')))
1883		{
1884			// are they allowed to access the fieldtypes manager?
1885			if ($DSP->allowed_group('can_admin_utilities'))
1886			{
1887				$LANG->fetch_language_file('fieldframe');
1888				$out = preg_replace('/(<li><a href=.+C=admin&amp;M=utilities&amp;P=extensions_manager.+<\/a><\/li>)/',
1889					"$1\n<li>".$DSP->anchor(BASE.AMP.'C=admin'.AMP.'M=utilities'.AMP.'P=fieldtypes_manager', $LANG->line('fieldtypes_manager')).'</li>', $out, 1);
1890			}
1891		}
1892
1893		foreach($this->snippets as $placement => $snippets)
1894		{
1895			$placement = '</'.$placement.'>';
1896			foreach(array_unique($snippets) as $snippet)
1897			{
1898				$out = str_replace($placement, NL.$snippet.NL.$placement, $out);
1899			}
1900		}
1901
1902		$args = func_get_args();
1903		return $this->forward_ff_hook('show_full_control_panel_end', $args, $out);
1904	}
1905
1906	/**
1907	 * Publish Form - Unique Field
1908	 *
1909	 * Allows adding of unique custom fields via extensions
1910	 *
1911	 * @param  array   $row  Parameters for the field from the database
1912	 * @param  array   $field_data  If entry is not new, this will have field's current value
1913	 * @return string  The field's HTML
1914	 * @see    http://expressionengine.com/developers/extension_hooks/publish_form_field_unique/
1915	 */
1916	function publish_form_field_unique($row, $field_data)
1917	{
1918		global $REGX, $DSP;
1919
1920		// return if this isn't a FieldFrame fieldtype
1921		if (substr($row['field_type'], 0, 9) != 'ftype_id_')
1922		{
1923			return $this->get_last_call();
1924		}
1925
1926		$field_name = 'field_id_'.$row['field_id'];
1927		$fields = $this->_get_fields();
1928
1929		if (array_key_exists($row['field_id'], $fields))
1930		{
1931			$field = $fields[$row['field_id']];
1932
1933			// is there post data available?
1934			if (isset($_POST[$field_name])) $field_data = $_POST[$field_name];
1935
1936			$this->row = $row;
1937			$r = $DSP->qdiv('ff-ft', $field['ftype']->display_field($field_name, $this->_unserialize($field_data), $field['settings']));
1938			unset($this->row);
1939		}
1940
1941		// place field data in a basic textfield if the fieldtype
1942		// wasn't enabled or didn't have a display_field method
1943		if ( ! isset($r))
1944		{
1945			$r = $DSP->input_textarea($field_name, $field_data, 1, 'textarea', '100%');
1946		}
1947
1948		$r = '<input type="hidden" name="field_ft_'.$row['field_id'].'" value="none" />'.$r;
1949
1950		$args = func_get_args();
1951		return $this->forward_ff_hook('publish_form_field_unique', $args, $r);
1952	}
1953
1954	/**
1955	 * Publish Form - Submit New Entry - Start
1956	 *
1957	 * Add More Stuff to do when you first submit an entry
1958	 *
1959	 * @see http://expressionengine.com/developers/extension_hooks/submit_new_entry_start/
1960	 */
1961	function submit_new_entry_start()
1962	{
1963		$this->_save_fields();
1964
1965		return $this->forward_ff_hook('submit_new_entry_start');
1966	}
1967
1968	/**
1969	 * Save Fields
1970	 *
1971	 * @access private
1972	 */
1973	function _save_fields()
1974	{
1975		foreach($this->_get_fields() as $this->field_id => $field)
1976		{
1977			$this->field_name = 'field_id_'.$this->field_id;
1978
1979			if (isset($_POST[$this->field_name]))
1980			{
1981				if (method_exists($field['ftype'], 'save_field'))
1982				{
1983					if ($field['ftype']->postpone_saves)
1984					{
1985						// save it for later
1986						$field['data'] = $_POST[$this->field_name];
1987						$this->postponed_saves[$this->field_id] = $field;
1988
1989						// prevent the system from overwriting the current data
1990						unset($_POST[$this->field_name]);
1991					}
1992					else
1993					{
1994						$_POST[$this->field_name] = $field['ftype']->save_field($_POST[$this->field_name], $field['settings']);
1995					}
1996				}
1997
1998				if (isset($_POST[$this->field_name]) AND is_array($_POST[$this->field_name]))
1999				{
2000					// serialize for DB storage
2001					$_POST[$this->field_name] = $_POST[$this->field_name]
2002					  ?  $this->_serialize($_POST[$this->field_name])
2003					  :  '';
2004				}
2005
2006				// unset extra FF post vars
2007				$prefix = $this->field_name.'_';
2008				$length = strlen($prefix);
2009				foreach($_POST as $key => $value)
2010				{
2011					if (substr($key, 0, $length) == $prefix)
2012					{
2013						unset($_POST[$key]);
2014					}
2015				}
2016			}
2017		}
2018
2019		if (isset($this->field_id)) unset($this->field_id);
2020		if (isset($this->field_name)) unset($this->field_name);
2021	}
2022
2023	/**
2024	 * Publish Form - Submit New Entry - End
2025	 *
2026	 * After an entry is submitted, do more processing
2027	 *
2028	 * @param string  $entry_id      Entry's ID
2029	 * @param array   $data          Array of data about entry (title, url_title)
2030	 * @param string  $ping_message  Error message if trackbacks or pings have failed to be sent
2031	 * @see   http://expressionengine.com/developers/extension_hooks/submit_new_entry_end/
2032	 */
2033	function submit_new_entry_end($entry_id, $data, $ping_message)
2034	{
2035		$this->_postponed_save($entry_id);
2036
2037		$args = func_get_args();
2038		return $this->forward_ff_hook('submit_new_entry_end', $args);
2039	}
2040
2041	/**
2042	 * Publish Form - Start
2043	 *
2044	 * Allows complete rewrite of Publish page
2045	 *
2046	 * @param  string  $which             new, preview, edit, or save
2047	 * @param  string  $submission_error  submission error, if any
2048	 * @param  string  $entry_id          Entry ID being sent to the form
2049	 * @see    http://expressionengine.com/developers/extension_hooks/publish_form_start/
2050	 */
2051	function publish_form_start($which, $submission_error, $entry_id)
2052	{
2053		global $IN;
2054
2055		$this->which = $which;
2056
2057		// is this a quicksave/preview?
2058		if (in_array($this->which, array('save', 'preview')))
2059		{
2060			if ($this->which == 'preview')
2061			{
2062				// submit_new_entry_start() doesn't get called on preview
2063				// so fill in for it here
2064				$this->_save_fields();
2065			}
2066
2067			if ( ! $entry_id) $entry_id = $IN->GBL('entry_id');
2068			$this->_postponed_save($entry_id);
2069		}
2070
2071		unset($this->which);
2072
2073		$args = func_get_args();
2074		return $this->forward_ff_hook('publish_form_start', $args);
2075	}
2076
2077	/**
2078	 * Postponed Save
2079	 *
2080	 * @access private
2081	 */
2082	function _postponed_save($entry_id)
2083	{
2084		global $DB;
2085
2086		foreach($this->postponed_saves as $this->field_id => $field)
2087		{
2088			$this->field_name = 'field_id_'.$this->field_id;
2089
2090			$_POST[$this->field_name] = $field['ftype']->save_field($field['data'], $field['settings'], $entry_id);
2091
2092			if (is_array($_POST[$this->field_name]))
2093			{
2094				$_POST[$this->field_name] = $_POST[$this->field_name]
2095				  ?  $this->_serialize($_POST[$this->field_name])
2096				  :  '';
2097			}
2098
2099			// manually save it to the db
2100			if ($entry_id && (! isset($this->which) || $this->which == 'save'))
2101			{
2102				$DB->query($DB->update_string('exp_weblog_data', array($this->field_name => $_POST[$this->field_name]), 'entry_id = '.$entry_id));
2103			}
2104		}
2105
2106		if (isset($this->field_id)) unset($this->field_id);
2107		if (isset($this->field_name)) unset($this->field_name);
2108	}
2109
2110	/**
2111	 * Weblog - SAEF - Start
2112	 *
2113	 * Rewrite the SAEF completely
2114	 *
2115	 * @param bool    $return_form  Return the No Cache version of the form or not
2116	 * @param string  $captcha  Cached CAPTCHA value from preview
2117	 * @param string  $weblog_id  Weblog ID for this form
2118	 * @see   http://expressionengine.com/developers/extension_hooks/weblog_standalone_form_start/
2119	 */
2120	function weblog_standalone_form_start($return_form, $captcha, $weblog_id)
2121	{
2122		global $DSP, $DB;
2123
2124		// initialize Display
2125		if ( ! $DSP)
2126		{
2127			if ( ! class_exists('Display'))
2128			{
2129				require PATH_CP.'cp.display'.EXT;
2130			}
2131			$DSP = new Display();
2132		}
2133
2134		// remember this is a SAEF for publish_form_field_unique
2135		$this->saef = TRUE;
2136		$this->saef_tag_count = 0;
2137
2138		$args = func_get_args();
2139		return $this->forward_ff_hook('weblog_standalone_form_start', $args);
2140	}
2141
2142	/**
2143	 * Weblog - SAEF - End
2144	 *
2145	 * Allows adding to end of submission form
2146	 *
2147	 * @param  string  $tagdata  The tag data for the submission form at the end of processing
2148	 * @return string  Modified $tagdata
2149	 * @see    http://expressionengine.com/developers/extension_hooks/weblog_standalone_form_end/
2150	 */
2151	function weblog_standalone_form_end($tagdata)
2152	{
2153		global $DSP;
2154
2155		$tagdata = $this->get_last_call($tagdata);
2156
2157		// parse fieldtype tags
2158		$tagdata = $this->weblog_entries_tagdata($tagdata);
2159
2160		$all_snippets = array();
2161
2162		foreach($this->snippets as $placement => $snippets)
2163		{
2164			$all_snippets = array_merge($all_snippets, array_unique($snippets));
2165
2166			$this->snippets[$placement] = array();
2167		}
2168
2169		//$tagdata = str_replace('</form>', '</form>'.implode(NL, $all_snippets), $tagdata);
2170		$tagdata .= NL.implode(NL, $all_snippets).NL;
2171
2172		$this->saef = FALSE;
2173		unset($this->saef_tag_count);
2174
2175		$args = func_get_args();
2176		return $this->forward_ff_hook('weblog_standalone_form_end', $args, $tagdata);
2177	}
2178
2179	/**
2180	 * Weblog - Entry Tag Data
2181	 *
2182	 * Modify the tagdata for the weblog entries before anything else is parsed
2183	 *
2184	 * @param  string   $tagdata   The Weblog Entries tag data
2185	 * @param  array    $row       Array of data for the current entry
2186	 * @param  object   $weblog    The current Weblog object including all data relating to categories and custom fields
2187	 * @return string              Modified $tagdata
2188	 * @see    http://expressionengine.com/developers/extension_hooks/weblog_entries_tagdata/
2189	 */
2190	function weblog_entries_tagdata($tagdata, $row=array(), $weblog=NULL)
2191	{
2192		global $REGX;
2193
2194		$this->tagdata = $this->get_last_call($tagdata);
2195		$this->row = $row;
2196		$this->weblog = &$weblog;
2197
2198		if ($fields = $this->_get_fields())
2199		{
2200			$fields_by_name = array();
2201			foreach($fields as $this->field_id => $this->field)
2202			{
2203				$fields_by_name[$this->field['name']] = array(
2204					'data'     => (isset($row['field_id_'.$this->field_id]) ? $this->_unserialize($row['field_id_'.$this->field_id], FALSE) : ''),
2205					'settings' => $this->field['settings'],
2206					'ftype'    => $this->field['ftype'],
2207					'helpers'  => array('field_id' => $this->field_id, 'field_name' => $this->field['name'])
2208				);
2209			}
2210
2211			if (isset($this->field_id)) unset($this->field_id);
2212			if (isset($this->field)) unset($this->field);
2213
2214			$this->_parse_tagdata($this->tagdata, $fields_by_name, TRUE);
2215		}
2216
2217		// unset temporary helper vars
2218		$tagdata = $this->tagdata;
2219		unset($this->tagdata);
2220		unset($this->row);
2221		unset($this->weblog);
2222
2223		$args = func_get_args();
2224		return $this->forward_ff_hook('weblog_entries_tagdata', $args, $tagdata);
2225	}
2226
2227	/**
2228	 * Parse Tagdata
2229	 *
2230	 * @param  string  $tagdata  The Weblog Entries tagdata
2231	 * @param  string  $field_name  Name of the field to search for
2232	 * @param  mixed   $field_data  The field's value
2233	 * @param  array   $field_settings  The field's settings
2234	 * @param  object  $ftype  The field's fieldtype object
2235	 * @access private
2236	 */
2237	function _parse_tagdata(&$tagdata, $fields, $skip_unmatched_tags = FALSE)
2238	{
2239		global $FNS, $DSP;
2240
2241		// -------------------------------------------
2242		//  Tag parsing
2243		// -------------------------------------------
2244
2245		// find the next ftype tag
2246		$offset = 0;
2247		while (preg_match('/'.LD.'('.implode('|', array_keys($fields)).')(:(\w+))?(\s+.*)?'.RD.'/sU', $tagdata, $matches, PREG_OFFSET_CAPTURE, $offset))
2248		{
2249			$field_name = $matches[1][0];
2250			$field = $fields[$field_name];
2251
2252			$tag_pos = $matches[0][1];
2253			$tag_len = strlen($matches[0][0]);
2254			$tagdata_pos = $tag_pos + $tag_len;
2255			$endtag = LD.SLASH.$field_name.(isset($matches[2][0]) ? $matches[2][0] : '').RD;
2256			$endtag_len = strlen($endtag);
2257			$endtag_pos = strpos($tagdata, $endtag, $tagdata_pos);
2258			$tag_func = (isset($matches[3][0]) AND $matches[3][0]) ? $matches[3][0] : '';
2259
2260			// is this SAEF?
2261			if ($this->saef AND ! $tag_func)
2262			{
2263				// call display_field rather than display_tag
2264
2265				foreach($field['helpers'] as $name => $value)
2266				{
2267					$this->$name = $value;
2268				}
2269
2270				$new_tagdata = $DSP->qdiv('ff-ft', $field['ftype']->display_field('field_id_'.$field['helpers']['field_id'], $field['data'], $field['settings']));
2271
2272				foreach($field['helpers'] as $name => $value)
2273				{
2274					unset($this->$name);
2275				}
2276
2277				// update the tag count
2278				$this->saef_tag_count++;
2279			}
2280			else
2281			{
2282				if ( ! $tag_func) $tag_func = 'display_tag';
2283				$method_exists = method_exists($field['ftype'], $tag_func);
2284
2285				if ($method_exists || ! $skip_unmatched_tags)
2286				{
2287					// get the params
2288					$params = isset($field['ftype']->default_tag_params)
2289					  ?  $field['ftype']->default_tag_params
2290					  :  array();
2291					if (isset($matches[4][0]) AND $matches[4][0] AND preg_match_all('/\s+([\w-:]+)\s*=\s*([\'\"])([^\2]*)\2/sU', $matches[4][0], $param_matches))
2292					{
2293						for ($j = 0; $j < count($param_matches[0]); $j++)
2294						{
2295							$params[$param_matches[1][$j]] = $param_matches[3][$j];
2296						}
2297					}
2298
2299					// is this a tag pair?
2300					$field_tagdata = ($endtag_pos !== FALSE)
2301					  ?  substr($tagdata, $tagdata_pos, $endtag_pos - $tagdata_pos)
2302					  :  '';
2303
2304					if ( ! $tag_func) $tag_func = 'display_tag';
2305
2306					if ($method_exists)
2307					{
2308						foreach($field['helpers'] as $name => $value)
2309						{
2310							$this->$name = $value;
2311						}
2312
2313						$new_tagdata = (string) $field['ftype']->$tag_func($params, $field_tagdata, $field['data'], $field['settings']);
2314
2315						foreach($field['helpers'] as $name => $value)
2316						{
2317							unset($this->$name);
2318						}
2319					}
2320					else
2321					{
2322						$new_tagdata = $field['data'];
2323					}
2324				}
2325			}
2326
2327			if (isset($new_tagdata))
2328			{
2329				$offset = $tag_pos;
2330
2331				$tagdata = substr($tagdata, 0, $tag_pos)
2332				         . $new_tagdata
2333				         . substr($tagdata, ($endtag_pos !== FALSE ? $endtag_pos+$endtag_len : $tagdata_pos));
2334
2335				unset($new_tagdata);
2336			}
2337			else
2338			{
2339				$offset = $tag_pos + $tag_len;
2340			}
2341		}
2342
2343		// -------------------------------------------
2344		//  Conditionals
2345		// -------------------------------------------
2346
2347		$conditionals = array();
2348
2349		foreach ($fields as $name => $field)
2350		{
2351			$conditionals[$name] = is_array($field['data']) ? ($field['data'] ? '1' : '') : $field['data'];
2352		}
2353
2354		$tagdata = $FNS->prep_conditionals($tagdata, $conditionals);
2355	}
2356
2357	/**
2358	 * LG Addon Updater - Register a New Addon Source
2359	 *
2360	 * @param  array  $sources  The existing sources
2361	 * @return array  The new source list
2362	 * @see    http://leevigraham.com/cms-customisation/expressionengine/lg-addon-updater/
2363	 */
2364	function lg_addon_update_register_source($sources)
2365	{
2366		$sources = $this->get_last_call($sources);
2367
2368		if ($this->settings['check_for_updates'] == 'y')
2369		{
2370			// add FieldFrame source
2371			$source = 'http://pixelandtonic.com/ee/versions.xml';
2372			if ( ! in_array($source, $sources))
2373			{
2374				$sources[] = $source;
2375			}
2376
2377			// add ftype sources
2378			foreach($this->_get_ftypes() as $class_name => $ftype)
2379			{
2380				$source = $ftype->info['versions_xml_url'];
2381				if ($source AND ! in_array($source, $sources))
2382				{
2383					$sources[] = $source;
2384				}
2385			}
2386		}
2387
2388		$args = func_get_args();
2389		return $this->forward_ff_hook('lg_addon_update_register_source', $args, $sources);
2390	}
2391
2392	/**
2393	 * LG Addon Updater - Register a New Addon ID
2394	 *
2395	 * @param  array  $addons  The existing sources
2396	 * @return array  The new addon list
2397	 * @see    http://leevigraham.com/cms-customisation/expressionengine/lg-addon-updater/
2398	 */
2399	function lg_addon_update_register_addon($addons)
2400	{
2401		$addons = $this->get_last_call($addons);
2402
2403		if ($this->settings['check_for_updates'] == 'y')
2404		{
2405			// add FieldFrame
2406			$addons[FF_CLASS] = FF_VERSION;
2407
2408			// add ftypes
2409			foreach($this->_get_ftypes() as $class_name => $ftype)
2410			{
2411				$addons[$class_name] = $ftype->info['version'];
2412			}
2413		}
2414
2415		$args = func_get_args();
2416		return $this->forward_ff_hook('lg_addon_update_register_addon', $args, $addons);
2417	}
2418
2419}
2420
2421
2422/**
2423 * Settings Display Class
2424 *
2425 * Provides FieldFrame settings-specific display methods
2426 *
2427 * @package  FieldFrame
2428 * @author   Brandon Kelly <brandon@pixelandtonic.com>
2429 */
2430class Fieldframe_SettingsDisplay {
2431
2432	/**
2433	 * Fieldframe_SettingsDisplay Constructor
2434	 */
2435	function __construct()
2436	{
2437		// initialize Display Class
2438		global $DSP;
2439		if ( ! $DSP)
2440		{
2441			if ( ! class_exists('Display'))
2442			{
2443				require PATH_CP.'cp.display'.EXT;
2444			}
2445			$DSP = new Display();
2446		}
2447
2448		$this->block_count = 0;
2449	}
2450
2451	/**
2452	 * Open Settings Block
2453	 *
2454	 * @param  string  $title_line  The block's title
2455	 * @return string  The block's head
2456	 */
2457	function block($title_line=FALSE, $num_cols=2)
2458	{
2459		global $DSP;
2460
2461		$this->row_count = 0;
2462		$this->num_cols = $num_cols;
2463
2464		$r = $DSP->table_open(array(
2465		                        'class'  => 'tableBorder',
2466		                        'border' => '0',
2467		                        'style' => 'margin:'.($this->block_count ? '18px' : '0').' 0 0 0; width:100%;'.($title_line ? '' : ' border-top:1px solid #CACFD4;')
2468		                      ));
2469		if ($title_line)
2470		{
2471			$r .= $this->row(array($this->get_line($title_line)), 'tableHeading');
2472		}
2473
2474		$this->block_count++;
2475
2476		return $r;
2477	}
2478
2479	/**
2480	 * Close Settings Block
2481	 */
2482	function block_c()
2483	{
2484		global $DSP;
2485		return $DSP->table_c();
2486	}
2487
2488	/**
2489	 * Settings Row
2490	 *
2491	 * @param  array   $col_data   Each column's contents
2492	 * @param  string  $row_class  CSS class to be added to each cell
2493	 * @param  array   $attr       HTML attributes to add onto the <tr>
2494	 * @return string  The settings row
2495	 */
2496	function row($col_data, $row_class=NULL, $attr=array())
2497	{
2498		global $DSP;
2499
2500		// get the alternating row class
2501		if ($row_class === NULL)
2502		{
2503			$this->row_count++;
2504			$this->row_class = ($this->row_count % 2)
2505			 ? 'tableCellOne'
2506			 : 'tableCellTwo';
2507		}
2508		else
2509		{
2510			$this->row_class = $row_class;
2511		}
2512
2513		$r = '<tr';
2514		foreach($attr as $key => $value) $r .= ' '.$key.'="'.$value.'"';
2515		$r .= '>';
2516		$num_cols = count($col_data);
2517		foreach($col_data as $i => $col)
2518		{
2519			$width = ($i == 0)
2520			  ?  '50%'
2521			  :  ($i < $num_cols-1 ? floor(50/($num_cols-1)).'%' : '');
2522			$colspan = ($i == $num_cols-1) ? $this->num_cols - $i : NULL;
2523			$r .= $DSP->td($this->row_class, $width, $colspan)
2524			    . $col
2525			    . $DSP->td_c();
2526		}
2527		$r .= $DSP->tr_c();
2528		return $r;
2529	}
2530
2531	/**
2532	 * Heading Row
2533	 *
2534	 * @param  array   $cols  Each column's heading line
2535	 * @return string  The settings heading row
2536	 */
2537	function heading_row($cols)
2538	{
2539		return $this->row($cols, 'tableHeadingAlt');
2540	}
2541
2542	/**
2543	 * Info Row
2544	 *
2545	 * @param  string  $info_line  Info text
2546	 * @return string  The settings info row
2547	 */
2548	function info_row($info_line, $styles=TRUE)
2549	{
2550		return $this->row(array(
2551		                   '<div class="box"' . ($styles ? ' style="border-width:0 0 1px 0; margin:0; padding:10px 5px"' : '') . '>'
2552		                 . '<p>'.$this->get_line($info_line).'</p>'
2553		                 . '</div>'
2554		                  ), '');
2555	}
2556
2557	/**
2558	 * Label
2559	 *
2560	 * @param  string  $label_line    The main label text
2561	 * @param  string  $subtext_line  The label's subtext
2562	 * @return string  The label
2563	 */
2564	function label($label_line, $subtext_line='')
2565	{
2566		global $DSP;
2567		$r = $DSP->qdiv('defaultBold', $this->get_line($label_line));
2568		if ($subtext_line) $r .= $DSP->qdiv('subtext', $this->get_line($subtext_line));
2569		return $r;
2570	}
2571
2572	/**
2573	 * Settings Text Input
2574	 *
2575	 * @param  string  $name   Name of the text field
2576	 * @param  string  $value  Initial value
2577	 * @param  array   $attr   Input variables
2578	 * @return string  The text field
2579	 */
2580	function text($name, $value, $attr=array())
2581	{
2582		global $DSP;
2583		$attr = array_merge(array('size'=>'','maxlength'=>'','style'=>'input','width'=>'90%','extras'=>'','convert'=>FALSE), $attr);
2584		return $DSP->input_text($name, $value, $attr['size'], $attr['maxlength'], $attr['style'], $attr['width'], $attr['extras'], $attr['convert']);
2585	}
2586
2587	/**
2588	 * Textarea
2589	 *
2590	 * @param  string  $name   Name of the textarea
2591	 * @param  string  $value  Initial value
2592	 * @param  array   $attr   Input variables
2593	 * @return string  The textarea
2594	 */
2595	function textarea($name, $value, $attr=array())
2596	{
2597		global $DSP;
2598		$attr = array_merge(array('rows'=>'3','style'=>'textarea','width'=>'91%','extras'=>'','convert'=>FALSE), $attr);
2599		return $DSP->input_textarea($name, $value, $attr['rows'], $attr['style'], $attr['width'], $attr['extras'], $attr['convert']);
2600	}
2601
2602	/**
2603	 * Select Options
2604	 *
2605	 * @param  string  $value    initial selected value(s)
2606	 * @param  array   $options  list of the options
2607	 * @return string  the select/multi-select options HTML
2608	 */
2609	function _select_options($value, $options)
2610	{
2611		global $DSP;
2612
2613		$r = '';
2614		foreach($options as $option_value => $option_line)
2615		{
2616			if (is_array($option_line))
2617			{
2618				$r .= '<optgroup label="'.$option_value.'">'."\n"
2619				    .   $this->_select_options($value, $option_line)
2620				    . '</optgroup>'."\n";
2621			}
2622			else
2623			{
2624				$selected = is_array($value)
2625				              ?  in_array($option_value, $value)
2626				              :  ($option_value == $value);
2627				$r .= $DSP->input_select_option($option_value, $this->get_line($option_line), $selected ? 1 : 0);
2628			}
2629		}
2630		return $r;
2631	}
2632
2633	/**
2634	 * Select input
2635	 *
2636	 * @param  string  $name     Name of the select
2637	 * @param  mixed   $value    Initial selected value(s)
2638	 * @param  array   $options  List of the options
2639	 * @param  array   $attr     Input variables
2640	 * @return string  The select input
2641	 */
2642	function select($name, $value, $options, $attr=array())
2643	{
2644		global $DSP;
2645		$attr = array_merge(array('multi'=>NULL, 'size'=>0, 'width'=>''), $attr);
2646		return $DSP->input_select_header($name, $attr['multi'], $attr['size'], $attr['width'])
2647		     . $this->_select_options($value, $options)
2648		     . $DSP->input_select_footer();
2649	}
2650
2651	/**
2652	 * Multiselect Input
2653	 *
2654	 * @param  string  $name     Name of the textfield
2655	 * @param  array   $values   Initial selected values
2656	 * @param  array   $options  List of the options
2657	 * @param  array   $attr     Input variables
2658	 * @return string  The multiselect input
2659	 */
2660	function multiselect($name, $values, $options, $attr=array())
2661	{
2662		$attr = array_merge($attr, array('multi' => 1));
2663		return $this->select($name, $values, $options, $attr);
2664	}
2665
2666	/**
2667	 * Radio Group
2668	 *
2669	 * @param  string  $name     Name of the radio inputs
2670	 * @param  string  $value    Initial selected value
2671	 * @param  array   $options  List of the options
2672	 * @param  array   $attr     Input variables
2673	 * @return string  The text input
2674	 */
2675	function radio_group($name, $value, $options, $attr=array())
2676	{
2677		global $DSP;
2678		$attr = array_merge(array('extras'=>''), $attr);
2679		$r = '';
2680		foreach($options as $option_value => $option_name)
2681		{
2682			if ($r) $r .= NBS.NBS.' ';
2683			$r .= '<label style="white-space:nowrap;">'
2684			    . $DSP->input_radio($name, $option_value, ($option_value == $value) ? 1 : 0, $attr['extras'])
2685			    . ' '.$this->get_line($option_name)
2686			    . '</label>';
2687		}
2688		return $r;
2689	}
2690
2691	/**
2692	 * Get Line
2693	 *
2694	 * @param  string  $line  unlocalized string or the name of a $LANG line
2695	 * @return string  Localized string
2696	 */
2697	function get_line($line)
2698	{
2699		global $FF;
2700		return $FF->get_line($line);
2701	}
2702
2703}
2704
2705/**
2706 * Fieldframe Fieldtype Base Class
2707 *
2708 * Provides FieldFrame fieldtypes with a couple handy methods
2709 *
2710 * @package  FieldFrame
2711 * @author   Brandon Kelly <brandon@pixelandtonic.com>
2712 */
2713class Fieldframe_Fieldtype {
2714
2715	var $_fieldframe = TRUE;
2716
2717	function get_last_call($param=FALSE)
2718	{
2719		global $FF;
2720		return $FF->get_last_call($param);
2721	}
2722
2723	function insert($at, $html)
2724	{
2725		global $FF;
2726		$FF->snippets[$at][] = $html;
2727	}
2728
2729	function insert_css($css)
2730	{
2731		$this->insert('head', '<style type="text/css" charset="utf-8">'.NL.$css.NL.'</style>');
2732	}
2733
2734	function insert_js($js)
2735	{
2736		$this->insert('body', '<script type="text/javascript">;'.NL.$js.NL.'</script>');
2737	}
2738
2739	function include_css($filename)
2740	{
2741		$this->insert('head', '<link rel="stylesheet" type="text/css" href="'.FT_URL.$this->_class_name.'/'.$filename.'" charset="utf-8" />');
2742	}
2743
2744	function include_js($filename)
2745	{
2746		$this->insert('body', '<script type="text/javascript" src="'.FT_URL.$this->_class_name.'/'.$filename.'" charset="utf-8"></script>');
2747	}
2748
2749	function options_setting($options=array(), $indent = '')
2750	{
2751		$r = '';
2752
2753		foreach($options as $name => $label)
2754		{
2755			if ($r !== '') $r .= "\n";
2756
2757			// force strings
2758			$name = (string) $name;
2759			$label = (string) $label;
2760
2761			// is this just a blank option?
2762			if ($name === '' && $label === '') $name = $label = ' ';
2763
2764			$r .= $indent . htmlentities($name, ENT_COMPAT, 'UTF-8');
2765
2766			// is this an optgroup?
2767			if (is_array($label))
2768			{
2769				$r .= "\n".$this->options_setting($label, $indent.'    ');
2770			}
2771			else if ($name !== $label)
2772			{
2773				$r .= ' : '.$label;
2774			}
2775		}
2776
2777		return $r;
2778	}
2779
2780	function save_options_setting($options = '', $total_levels = 1)
2781	{
2782		// prepare options
2783		$options = preg_split('/[\r\n]+/', $options);
2784		foreach($options as &$option)
2785		{
2786			$option_parts = preg_split('/\s:\s/', $option, 2);
2787			$option = array();
2788			$option['indent'] = preg_match('/^\s+/', $option_parts[0], $matches) ? strlen(str_replace("\t", '    ', $matches[0])) : 0;
2789			$option['name']   = trim($option_parts[0]);
2790			$option['value']  = isset($option_parts[1]) ? trim($option_parts[1]) : $option['name'];
2791		}
2792
2793		return $this->_structure_options($options, $total_levels);
2794	}
2795
2796	function _structure_options(&$options, $total_levels, $level = 1, $indent = -1)
2797	{
2798		$r = array();
2799
2800		while ($options)
2801		{
2802			if ($indent == -1 || $options[0]['indent'] > $indent)
2803			{
2804				$option = array_shift($options);
2805				$children = ( ! $total_levels OR $level < $total_levels)
2806				              ?  $this->_structure_options($options, $total_levels, $level+1, $option['indent']+1)
2807				              :  FALSE;
2808				$r[(string)$option['name']] = $children ? $children : (string)$option['value'];
2809			}
2810			else if ($options[0]['indent'] <= $indent)
2811			{
2812				break;
2813			}
2814		}
2815
2816		return $r;
2817	}
2818
2819	function prep_iterators(&$tagdata)
2820	{
2821		// find {switch} tags
2822		$this->_switches = array();
2823		$tagdata = preg_replace_callback('/'.LD.'switch\s*=\s*([\'\"])([^\1]+)\1'.RD.'/sU', array(&$this, '_get_switch_options'), $tagdata);
2824
2825		$this->_count_tag = 'count';
2826		$this->_iterator_count = 0;
2827	}
2828
2829	function _get_switch_options($match)
2830	{
2831		global $FNS;
2832
2833		$marker = LD.'SWITCH['.$FNS->random('alpha', 8).']SWITCH'.RD;
2834		$this->_switches[] = array('marker' => $marker, 'options' => explode('|', $match[2]));
2835		return $marker;
2836	}
2837
2838	function parse_iterators(&$tagdata)
2839	{
2840		global $TMPL;
2841
2842		// {switch} tags
2843		foreach($this->_switches as $i => $switch)
2844		{
2845			$option = $this->_iterator_count % count($switch['options']);
2846			$tagdata = str_replace($switch['marker'], $switch['options'][$option], $tagdata);
2847		}
2848
2849		// update the count
2850		$this->_iterator_count++;
2851
2852		// {count} tags
2853		$tagdata = $TMPL->swap_var_single($this->_count_tag, $this->_iterator_count, $tagdata);
2854	}
2855
2856}
2857
2858/**
2859 * Fieldframe Multi-select Fieldtype Base Class
2860 *
2861 * Provides Multi-select fieldtypes with their base functionality
2862 *
2863 * @package  FieldFrame
2864 * @author   Brandon Kelly <brandon@pixelandtonic.com>
2865 */
2866class Fieldframe_Multi_Fieldtype extends Fieldframe_Fieldtype {
2867
2868	var $default_field_settings = array(
2869		'options' => array(
2870			'Option 1' => 'Option 1',
2871			'Option 2' => 'Option 2',
2872			'Option 3' => 'Option 3'
2873		)
2874	);
2875
2876	var $default_cell_settings = array(
2877		'options' => array(
2878			'Opt 1' => 'Opt 1',
2879			'Opt 2' => 'Opt 2'
2880		)
2881	);
2882
2883	var $default_tag_params = array(
2884		'sort'      => '',
2885		'backspace' => '0'
2886	);
2887
2888	var $settings_label = 'field_list_items';
2889	var $total_option_levels = 1;
2890
2891	/**
2892	 * Display Field Settings
2893	 * 
2894	 * @param  array  $field_settings  The field's settings
2895	 * @return array  Settings HTML (cell1, cell2, rows)
2896	 */
2897	function display_field_settings($field_settings)
2898	{
2899		global $DSP, $LANG;
2900
2901		$cell2 = $DSP->qdiv('defaultBold', $LANG->line($this->settings_label))
2902		       . $DSP->qdiv('default', $LANG->line('field_list_instructions'))
2903		       . $DSP->input_textarea('options', $this->options_setting($field_settings['options']), '6', 'textarea', '99%')
2904		       . $DSP->qdiv('default', $LANG->line('option_setting_examples'));
2905
2906		return array('cell2' => $cell2);
2907	}
2908
2909	/**
2910	 * Display Cell Settings
2911	 * 
2912	 * @param  array  $cell_settings  The cell's settings
2913	 * @return string  Settings HTML
2914	 */
2915	function display_cell_settings($cell_settings)
2916	{
2917		global $FFM, $DSP, $LANG;
2918
2919		if (version_compare($FFM->info['version'], '2.0', '<'))
2920		{
2921			return '<label class="itemWrapper">'
2922			       . $DSP->qdiv('defaultBold', $LANG->line($this->settings_label))
2923			       . $DSP->input_textarea('options', $this->options_setting($cell_settings['options']), '4', 'textarea', '140px')
2924			       . '</label>';
2925		}
2926
2927		return array(
2928			array(
2929				$LANG->line($this->settings_label),
2930				'<textarea class="matrix-textarea" name="options" rows="4">'.$this->options_setting($cell_settings['options']).'</textarea>'
2931			)
2932		);
2933	}
2934
2935	/**
2936	 * Save Field Settings
2937	 *
2938	 * Turn the options textarea value into an array of option names and labels
2939	 * 
2940	 * @param  array  $field_settings  The user-submitted settings, pulled from $_POST
2941	 * @return array  Modified $field_settings
2942	 */
2943	function save_field_settings($field_settings)
2944	{
2945		$field_settings['options'] = $this->save_options_setting($field_settings['options'], $this->total_option_levels);
2946		return $field_settings;
2947	}
2948
2949	/**
2950	 * Save Cell Settings
2951	 *
2952	 * Turn the options textarea value into an array of option names and labels
2953	 * 
2954	 * @param  array  $cell_settings  The user-submitted settings, pulled from $_POST
2955	 * @return array  Modified $cell_settings
2956	 */
2957	function save_cell_settings($cell_settings)
2958	{
2959		return $this->save_field_settings($cell_settings);
2960	}
2961
2962	/**
2963	 * Prep Field Data
2964	 *
2965	 * Ensures $field_data is an array.
2966	 *
2967	 * @param  mixed  &$field_data  The currently-saved $field_data
2968	 */
2969	function prep_field_data(&$field_data)
2970	{
2971		if ( ! is_array($field_data))
2972		{
2973			$field_data = array_filter(preg_split("/[\r\n]+/", $field_data));
2974		}
2975	}
2976
2977	function _find_option($needle, $haystack)
2978	{
2979		foreach ($haystack as $key => $value)
2980		{
2981			$r = $value;
2982			if ($needle == $key OR (is_array($value) AND (($r = $this->_find_option($needle, $value)) !== FALSE)))
2983			{
2984				return $r;
2985			}
2986		}
2987		return FALSE;
2988	}
2989
2990	/**
2991	 * Display Tag
2992	 *
2993	 * @param  array   $params          Name/value pairs from the opening tag
2994	 * @param  string  $tagdata         Chunk of tagdata between field tag pairs
2995	 * @param  string  $field_data      Currently saved field value
2996	 * @param  array   $field_settings  The field's settings
2997	 * @return string  Modified $tagdata
2998	 */
2999	function display_tag($params, $tagdata, $field_data, $field_settings)
3000	{
3001		global $TMPL;
3002
3003		if ( ! $tagdata)
3004		{
3005			return $this->ul($params, $tagdata, $field_data, $field_settings);
3006		}
3007
3008		$this->prep_field_data($field_data);
3009		$r = '';
3010
3011		if ($field_settings['options'] AND $field_data)
3012		{
3013			// optional sorting
3014			if ($sort = strtolower($params['sort']))
3015			{
3016				if ($sort == 'asc')
3017				{
3018					sort($field_data);
3019				}
3020				else if ($sort == 'desc')
3021				{
3022					rsort($field_data);
3023				}
3024			}
3025
3026			// prepare for {switch} and {count} tags
3027			$this->prep_iterators($tagdata);
3028
3029			foreach($field_data as $option_name)
3030			{
3031				if (($option = $this->_find_option($option_name, $field_settings['options'])) !== FALSE)
3032				{
3033					// copy $tagdata
3034					$option_tagdata = $tagdata;
3035
3036					// simple var swaps
3037					$option_tagdata = $TMPL->swap_var_single('option', $option, $option_tagdata);
3038					$option_tagdata = $TMPL->swap_var_single('option_name', $option_name, $option_tagdata);
3039
3040					// parse {switch} and {count} tags
3041					$this->parse_iterators($option_tagdata);
3042
3043					$r .= $option_tagdata;
3044				}
3045			}
3046
3047			if ($params['backspace'])
3048			{
3049				$r = substr($r, 0, -$params['backspace']);
3050			}
3051		}
3052
3053		return $r;
3054	}
3055
3056	/**
3057	 * Unordered List
3058	 *
3059	 * @param  array   $params          Name/value pairs from the opening tag
3060	 * @param  string  $tagdata         Chunk of tagdata between field tag pairs
3061	 * @param  string  $field_data      Currently saved field value
3062	 * @param  array   $field_settings  The field's settings
3063	 * @return string  unordered list of options
3064	 */
3065	function ul($params, $tagdata, $field_data, $field_settings)
3066	{
3067		return "<ul>\n"
3068		     .   $this->display_tag($params, "  <li>{option}</li>\n", $field_data, $field_settings)
3069		     . '</ul>';
3070	}
3071
3072	/**
3073	 * Ordered List
3074	 *
3075	 * @param  array   $params          Name/value pairs from the opening tag
3076	 * @param  string  $tagdata         Chunk of tagdata between field tag pairs
3077	 * @param  string  $field_data      Currently saved field value
3078	 * @param  array   $field_settings  The field's settings
3079	 * @return string  ordered list of options
3080	 */
3081	function ol($params, $tagdata, $field_data, $field_settings)
3082	{
3083		return "<ol>\n"
3084		     .   $this->display_tag($params, "  <li>{option}</li>\n", $field_data, $field_settings)
3085		     . '</ol>';
3086	}
3087
3088	/**
3089	 * All Options
3090	 *
3091	 * @param  array   $params          Name/value pairs from the opening tag
3092	 * @param  string  $tagdata         Chunk of tagdata between field tag pairs
3093	 * @param  string  $field_data      Currently saved field value
3094	 * @param  array   $field_settings  The field's settings
3095	 * @return string  Modified $tagdata
3096	 */
3097	function all_options($params, $tagdata, $field_data, $field_settings, $iterator_count = 0)
3098	{
3099		global $TMPL;
3100
3101		Fieldframe_Multi_Fieldtype::prep_field_data($field_data);
3102		$r = '';
3103
3104		if ($field_settings['options'])
3105		{
3106			// optional sorting
3107			if ($sort = strtolower($params['sort']))
3108			{
3109				if ($sort == 'asc')
3110				{
3111					asort($field_settings['options']);
3112				}
3113				else if ($sort == 'desc')
3114				{
3115					arsort($field_settings['options']);
3116				}
3117			}
3118
3119			// prepare for {switch} and {count} tags
3120			$this->prep_iterators($tagdata);
3121			$this->_iterator_count += $iterator_count;
3122
3123			foreach($field_settings['options'] as $option_name => $option)
3124			{
3125				if (is_array($option))
3126				{
3127					$r .= $this->all_options(array_merge($params, array('backspace' => '0')), $tagdata, $field_data, array('options' => $option), $this->_iterator_count);
3128				}
3129				else
3130				{
3131					// copy $tagdata
3132					$option_tagdata = $tagdata;
3133
3134					// simple var swaps
3135					$option_tagdata = $TMPL->swap_var_single('option', $option, $option_tagdata);
3136					$option_tagdata = $TMPL->swap_var_single('option_name', $option_name, $option_tagdata);
3137					$option_tagdata = $TMPL->swap_var_single('selected', (in_array($option_name, $field_data) ? 1 : 0), $option_tagdata);
3138
3139					// parse {switch} and {count} tags
3140					$this->parse_iterators($option_tagdata);
3141
3142					$r .= $option_tagdata;
3143				}
3144			}
3145
3146			if ($params['backspace'])
3147			{
3148				$r = substr($r, 0, -$params['backspace']);
3149			}
3150		}
3151
3152		return $r;
3153	}
3154
3155	/**
3156	 * Is Selected?
3157	 *
3158	 * @param  array   $params          Name/value pairs from the opening tag
3159	 * @param  string  $tagdata         Chunk of tagdata between field tag pairs
3160	 * @param  string  $field_data      Currently saved field value
3161	 * @param  array   $field_settings  The field's settings
3162	 * @return bool    whether or not the option is selected
3163	 */
3164	function selected($params, $tagdata, $field_data, $field_settings)
3165	{
3166		$this->prep_field_data($field_data);
3167
3168		return (isset($params['option']) AND in_array($params['option'], $field_data)) ? 1 : 0;
3169	}
3170
3171	/**
3172	 * Total Selections
3173	 */
3174	function total_selections($params, $tagdata, $field_data, $field_settings)
3175	{
3176		$this->prep_field_data($field_data);
3177
3178		return $field_data ? (string)count($field_data) : '0';
3179	}
3180
3181}