Unnamed Fossil Project

Check-in [24768f2ba3]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:first import...
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA1:24768f2ba335b49f849505ea04dbbaa67886e65d
User & Date: root 2013-04-01 21:29:53
Context
2013-04-03
00:57
Funkcni import... Leaf check-in: da1cb101a4 user: root tags: trunk
2013-04-01
21:29
first import... check-in: 24768f2ba3 user: root tags: trunk
21:17
initial empty check-in check-in: 7ffe413bf7 user: root tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Added index.html.

































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="content-type" content="text/html;charset=utf-8">
	<title>Zupy</title>
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<link href="st/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
	<style>
	body {padding: 60px;}
	</style>
</head>
<body>
	<div class="navbar navbar-fixed-top navbar-inverse">
		<div class="navbar-inner">
			<div class="container">
				<a class="brand" href="#">Zupy</a>
				<ul class="nav">
					<li class="active"><a href="#">Severáček</a></li>
					<li><a href="#"><i class="icon icon-envelope icon-white"></i> <span class="label label-importnant">6 new</span></a></li>
				</ul>
				<div class="btn-group pull-right">
					<a href="#" class="btn" title="Add"><i class="icon icon-plus"></i></a>
					<a href="#" class="btn" title="Edit mode"><i class="icon icon-pencil"></i></a>
					<a href="#" class="btn" title="Configuration"><i class="icon icon-wrench"></i></a>
					<a href="#" class="btn" title="Logout"><i class="icon icon-off"></i></a>
				</div>
			</div>
		</div>
	</div>
	<div class="container">
		<div class="row">
			<div class="span9">
				<div id="post-123">
					<div class="row">
						<div class="span2">
							<a href="#" title="A">
								<img src="st/imgs/ezhika.jpg" class="img-polaroid">
								<abbr>ezhika</abbr>
							</a>
							<div>
								<small>1970-01-01 22:33</small>
							</div>
						</div>
						<div class="span7">
							<blockquote>
								<p>Češi jsou v matematice nic moc.</p>
								<small>Pavel Kreamer - vlastenec</small>
							</blockquote>
							<div>
								<i class="icon icon-repeat"></i>reposted via <a href="#">ruzovy</a>
							</div>
						</div>
					</div>
					<div class="pull-right badge badge-inverse">
						<a href="#">#</a>
						&nbsp;
						<a href="#">repost</a>
						&nbsp;
						<a href="#">react</a>
					</div>
					<hr>
				</div>
				<div id="post-124">
					<div class="row">
						<div class="span2">
							<a href="#" title="B">
								<img src="st/imgs/klubicko.jpg" class="img-polaroid" alt="xichtb">
								<abbr>liska</abbr>
							</a>
							<div>
								<small>1970-01-01 22:33</small>
							</div>
						</div>
						<div class="span7">
							<img src="st/imgs/ezhika-full.jpeg">
							<div>
								<i class="icon icon-hand-right"></i> <a href="#">12</a> reactions
							</div>
							<div class="row">
								<div class="span1">
									<img src="st/imgs/klubicko.jpg" class="img-polaroid" alt="xichtb">
								</div>
								<div class="span2">
									<p>Nice One!</p>
								</div>
							</div>
							<hr>
							<div class="row">
								<div class="span1">
									<img src="st/imgs/ezhika.jpg" class="img-polaroid" alt="xichtb">
									
								</div>
								<div class="span2">
									<p>Thanks!</p>
								</div>
							</div>
						</div>
					</div>
					<div class="pull-right badge badge-inverse">
						<a href="#">#</a>
						&nbsp;
						<a href="#">repost</a>
						&nbsp;
						<a href="#">react</a>
					</div>
					<hr>
				</div>
				<div id="post-124">
					<div class="row">
						<div class="span2">
							<img src="st/imgs/klubicko.jpg" class="img-polaroid" alt="xichtb">
							<abbr>liska</abbr>
							<div>
								<small>1970-01-01 22:33</small>
							</div>
						</div>
						<div class="span7">
							<p>,,Vlastně jedinná zajímavá věc bylo to, že se tam našla pevrhnutá lahev rumu a tři skleničky. Ne dvě jak by jeden čekal.</p>
							<p>A hned tu máme problém tří těles! Znáte problém tří těles?" - ,,Něco už jsem o
tom slyšela" vzpomínala slečna ,,Ale už si nepamatuju, co to vlastně bylo" - ,,No - to je klasický problém nebeské mechaniky. Když máme jenom dvě tělesa, lze se vždycky dopočítat toho jak se budou chovat. Ale jakmile máme tři, problém je najednou téměř neřešitelný a vykazuje chaotické chování. A stejné je to i u lidí. Jenom jsou místo gravitačních sil vázaní svými city. Jedna duše něco udělá nebo řekne a ty zbylé dvě mění názory a výpovědi jako u Agáty Christie. Problém tří těles je tedy skoro neřešitelný i pro nás detektivy..." povzdechl si detektiv a zaostřil na vlas, který padal slečně do obličeje.
</p>
<p>,,Vlastně jsou z toho v kopru uplně všichni" dodal.</p>
						</div>
					</div>
					<div class="pull-right badge badge-inverse">
						<a href="#">#</a>
						&nbsp;
						<a href="#">repost</a>
						&nbsp;
						<a href="#">react</a>
					</div>
					<hr>
					<ul class="pager">
						<li class="next"><a href="#">Older &rarr;</a></li>
					</ul>
				</div>
			</div>
			<div class="span3">
				<h5>Severákova polívka</h5>
				<p>asi zelňačka, mainly in czech</p>
				Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel enim quis nibh ornare ornare eget blandit arcu. Vestibulum posuere, leo ac iaculis tincidunt, nunc leo gravida est, a varius lectus ligula a erat. Fusce auctor tincidunt orci ac lacinia. Integer aliquam, nulla quis cursus dictum, nunc magna sagittis dui, in lacinia leo ipsum vel leo. Quisque a lorem non nisl consectetur tincidunt sit amet vitae magna. Mauris ut feugiat ipsum. Proin orci tortor, consequat eu consectetur sed, malesuada eu lorem. Donec porttitor tincidunt neque, non tincidunt mauris placerat sit amet. Maecenas eget vestibulum est. Suspendisse tincidunt, velit a hendrerit consequat, metus felis vehicula quam, id vestibulum sem sapien et nisi. Etiam ac odio quis turpis sagittis lacinia. Mauris sit amet ultrices ligula. Suspendisse mauris lectus, bibendum scelerisque ultrices ac, blandit eu risus.
</p>
<p>
Duis tempor lacus a leo ornare quis lacinia est laoreet. Phasellus id luctus augue. Nam placerat scelerisque ante, at facilisis nisl molestie eu. Maecenas eget nulla id ligula posuere cursus non eget arcu. Curabitur adipiscing augue eget quam volutpat vestibulum. Nam euismod posuere tortor non molestie. Maecenas ultrices auctor ante. Aliquam at felis a nunc ullamcorper dictum id tristique dui. Sed tristique tellus ut tortor pharetra fermentum. Nunc sed nisi lorem, at ultricies felis. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Proin adipiscing vulputate metus fermentum pellentesque. Nam justo enim, mattis in dictum ut, pulvinar a tortor. Aliquam erat volutpat. Donec sit amet elementum urna.
</p>
<p>
Nunc fermentum dictum nibh. Sed et nibh interdum quam dapibus facilisis ut eget tellus. In hac habitasse platea dictumst. Vestibulum vitae tellus purus, eget molestie metus. Proin fringilla enim id quam porttitor mollis. Etiam tristique ultricies tellus, sed vulputate sapien elementum varius. Pellentesque scelerisque pulvinar dolor vel ornare. Sed arcu nunc, eleifend quis bibendum sit amet, sodales eu enim. Suspendisse potenti. Duis enim sapien, sollicitudin in tristique nec, interdum vitae libero. Quisque malesuada placerat turpis, id tincidunt justo vehicula porttitor. Curabitur neque enim, blandit in iaculis non, commodo ac tellus.
</p>
<p>
Donec ac mi ipsum. Nam in imperdiet tortor. Ut ut sapien tincidunt massa tempor dignissim. Nulla elit diam, ultrices vitae pharetra nec, consectetur sed ante. Etiam iaculis, tellus vitae ultrices tristique, nisi arcu malesuada enim, vitae porttitor nulla quam ut dui. Integer sagittis dictum purus ut ultrices. Vivamus at nisl nisi. Suspendisse sed metus ligula, at vestibulum dolor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nunc ultrices sagittis fermentum. Praesent vel tempor massa.
</p>
<p>
Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Suspendisse odio ante, tincidunt sed tincidunt a, interdum in elit. Integer a quam metus, non auctor lectus. Cras gravida fermentum arcu vel pulvinar. Vestibulum hendrerit pellentesque semper. Nulla vel ipsum tristique sem sagittis sodales sed at massa. Curabitur in purus nulla.
</p>
			</div>
		</div>
	</div>
	<script src="st/js/jquery-1.9.1.min.js"></script>
	<script src="st/bootstrap/js/bootstrap.min.js"></script>
</body>

Added index.php.



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$rootPath = dirname(__FILE__);
$libPath = $rootPath . '/lib';
header('text/plain; encodng: utf8');

require $libPath . '/brickyard.php';

$fw = new brickyard();
$fw->init();
$fw->inDevelMode = true;

$db = new fDatabase('sqlite', $rootPath . '/db/zupy.db3');
echo 'DB';
$import = new zupy_mirror_soup($db);
echo 'IMPORT';
$import->importFeed($rootPath . '/soup_severak.rss', 1);
echo 'OK';

Added lib/brick/saxofon.php.









































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
class brick_saxofon
{
	protected $_openCallbacks = array();
	protected $_closeCallbacks = array();
	protected $_textCallbacks = array();
	protected $_reader = array();

	function registerOpen($name, $callback)
	{
		$this->_openCallbacks[$name] = $callback;
	}
	
	function registerText($name, $callback)
	{
		$this->_textCallbacks[$name] = $callback;
	}
	
	function registerClose($name, $callback)
	{
		$this->_closeCallbacks[$name] = $callback;
	}

	function readFile($name)
	{
		$reader = new XMLReader();
		$reader->open($name);
		$stack = array();
		while ($reader->read()) {
			if ($reader->nodeType == XMLReader::ELEMENT) {
				if (!$reader->isEmptyElement) {
					array_push($stack, $reader->name);
					$n = join('/', $stack);
					if (isset($this->_openCallbacks[$n])) {
						call_user_func($this->_openCallbacks[$n]);
					}
				}
			} elseif ($reader->nodeType == XMLReader::TEXT) {
				$n = join('/', $stack);
				if (isset($this->_textCallbacks[$n])) {
					call_user_func($this->_textCallbacks[$n], $reader->value);
				}
			} elseif ($reader->nodeType == XMLReader::END_ELEMENT) {
				array_pop($stack);
				$n = join('/', $stack);
				if (isset($this->_closeCallbacks[$n])) {
					call_user_func($this->_closeCallbacks[$n]);
				}
			}
		}
	}
}

Added lib/brickyard.php.

























































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
<?php
//
// Brickyard framework by Severak
//
//I am not yet decided about license. But I prefer WTFPL.
class brickyard
{
	public $inDevelMode = false;
	public $router = null;
	public $view = null;
	public $logger = null;
	public $libPath = '.';
	public $indexPath = '.';
	public function __construct(){
		$this->router = new brickyard_router_default;
		$this->logger = new brickyard_logger_null;
		$this->libPath = dirname(__FILE__);
		$this->view = new brickyard_view_default(dirname(__FILE__) . DIRECTORY_SEPARATOR . "tpl");
	}
	
	public function getRouter()
	{
		return $this->router;
	}
	public function getView()
	{
		return $this->view;
	}
	public function getLogger()
	{
		return $this->logger;
	}
	public function getIndexPath()
	{
		return $this->indexPath;
	}
	
	public function setRouter(brickyard_router_interface $router)
	{
		$this->router = $router;
	}
	public function setView(brickyard_view_interface $view)
	{
		$this->view = $view;
	}
	public function setLogger(brickyard_logger_interface $logger)
	{
		$this->logger = $logger;
	}
	public function setIndexPath($indexFilePath)
	{
		$this->indexPath = dirname($indexFilePath);
	}
	
	
	public function autoload($className){
		$filename=$this->libPath . DIRECTORY_SEPARATOR;
		$filename.=str_replace("_", DIRECTORY_SEPARATOR, $className);
		$filename.=".php";
		if (file_exists($filename)){
			require $filename;
			if (!class_exists($className, false)){
				throw new brickyard_exception_autoload('Class ' . $className . ' expected to be in ' . $filename . '!');
			}
		} else {
			throw new brickyard_exception_autoload('Class ' . $className . ' not found! Tried to find it in ' . $filename . '.');
		}
		
	}
	
	function error_handler($errno, $errstr, $errfile, $errline )
	{
		throw new ErrorException($errstr, $errno, 0, $errfile, $errline);
		
	}
	
	public function exception_handler($e)
	{
		if ($this->inDevelMode){
			$this->bluescreen($e);
		} else {
			$this->logger->logException($e);
			
			if ($e instanceof brickyard_exception_404){
				$err = 404;
			} elseif ($e instanceof brickyard_exception_403){
				$err = 403;
			} else {
				$err = 'error';
			}	
			
			if (file_exists($this->libPath . DIRECTORY_SEPARATOR . $err . '.html')){
				ob_clean();
				echo file_get_contents($this->libPath . DIRECTORY_SEPARATOR . $err . '.html');
				exit; //to prevent more errors
			}else{
				echo "An error occured. Also error page is missing.";
			}
			
		}
		
	}
	
	public function shutdown_handler()
	{
		$error = error_get_last();
		if ($error['type'] & (E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR | E_PARSE)) {
			$fatal = new ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line']);
			$this->exception_handler($fatal);
		}
		
	}
	
	public function bluescreen($e)
	{
		ob_clean();
		$out="<html><head><title>error</title></head><body><h1>:-(</h1>";
		$out.="<div>at " . $e->getFile() . ':' . $e->getLine() . "</div>";
		$out.="<div>" . nl2br( $e->getMessage() ) . "</div>";
		$out.="<pre>" . $e->getTraceAsString() . "</pre>";
		$out.="</body></html>";
		echo $out;
		exit;
		
	}
	
	public function init(){
		spl_autoload_register(array($this,"autoload"));
		set_error_handler(array($this,"error_handler"));
		register_shutdown_function(array($this, "shutdown_handler"));
		set_exception_handler(array($this,"exception_handler"));
		
	}
	
	public function run(){
		ob_start();
		$controllerName = "c_" . $this->router->getController();
		$methodName = $this->router->getMethod();
		$args = $this->router->getArgs();
		try {
			$controllerInstance = new $controllerName;
		} catch(brickyard_exception_autoload $e) {
			throw new brickyard_exception_404($e->getMessage() );
		}
		$controllerInstance->framework=$this;
		$call=array($controllerInstance, $methodName);
		if (is_callable($call)){
			call_user_func_array($call,$args);
		}else{
			throw new brickyard_exception_404('Method ' . $methodName . ' is invalid!');
		}
		
	}
}

class brickyard_exception_autoload extends Exception{}

class brickyard_exception_404 extends Exception{}

class brickyard_exception_403 extends Exception{}

interface brickyard_router_interface

{

	public function getController();

	public function getMethod();

	public function getArgs();

	public function getLink($controller = null, $method = null, $args=array() );

	

}

class brickyard_router_default implements brickyard_router_interface

{

	public $controller = "home";

	public $method = "index";

	public $args = array();

	

	function analyze()

	{

		$path=( isset($_SERVER["PATH_INFO"]) ? explode("/",$_SERVER["PATH_INFO"]) : array() );

		if (count($path)>1 and $path[1]!=''){$this->controller=$path[1];}

		if (count($path)>2  and $path[2]!=''){$this->method=$path[2];}

		if (count($path)>3){$this->args=array_slice($path,3);}

	}

	

	public function getController()

	{

		$this->analyze();

		return $this->controller;

	}

	

	public function getMethod()

	{

		$this->analyze();

		return $this->method;

	}

	

	public function getArgs()

	{

		$this->analyze();

		return $this->args;

	}

	

	public function getLink($controller = null, $method = null, $args=array() )

	{

		$url = $_SERVER["SCRIPT_NAME"];

		if ($controller){

			$url .= '/' . $controller;

			if ($method){

				$url .= '/' . $method;

				if (count($args)>0){

					$url .= '/' . implode('/', $args);

				}

			}

		}

		return $url;

	}

}

interface brickyard_view_interface

{

	public function show($templateName, array $data);

}

class brickyard_view_default implements brickyard_view_interface

{

	private $tplPath="tpl";

	

	function __construct($tplPath)

	{

		$this->tplPath = $tplPath;

	}

	

	public function show($tplName, array $data)

	{

		$tplFile = $this->tplPath . DIRECTORY_SEPARATOR . $tplName . ".php";

		if (file_exists($tplFile)) {

			$data['view'] = $this;

			extract($data, EXTR_SKIP);

			ob_start();

			include $tplFile;

			$output = ob_get_contents();

			ob_end_clean();

			return $output;

		} else {

			throw new Exception('Template '.$tplName.' not found in file '.$tplFile);

		}

	}

}

interface brickyard_logger_interface

{

	public function logException(Exception $e);

}

class brickyard_logger_null implements brickyard_logger_interface

{

	function logException(Exception $e) {}

}

class brickyard_logger_file implements brickyard_logger_interface

{

	private $logFileName="log.txt";



	function __construct($logFileName)

	{

		$this->logFileName = $logFileName;

	}

	

	function logException(Exception $e)

	{

		$logged = '== ' . date('Y-m-d H:i:s') . PHP_EOL .

		$e->getMessage() . PHP_EOL .

		$e->getFile() . ':' . $e->getLine() . PHP_EOL .

		$e->getTraceAsString() . PHP_EOL;

		file_put_contents($this->logFileName, $logged, FILE_APPEND);

	}

}

Added lib/fActiveRecord.php.

















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
<?php
/**
 * An [http://en.wikipedia.org/wiki/Active_record_pattern active record pattern] base class
 * 
 * This class uses fORMSchema to inspect your database and provides an
 * OO interface to a single database table. The class dynamically handles
 * method calls for getting, setting and other operations on columns. It also
 * dynamically handles retrieving and storing related records.
 * 
 * @copyright  Copyright (c) 2007-2011 Will Bond, others
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @author     Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fActiveRecord
 * 
 * @version    1.0.0b81
 * @changes    1.0.0b81  Fixed a bug with updating a record that contains only an auto-incrementing primary key [wb, 2011-09-06]
 * @changes    1.0.0b80  Added support to ::checkCondition() for the `^~` and `$~` operators [wb, 2011-06-20]
 * @changes    1.0.0b79  Fixed some bugs in handling relationships between PHP 5.3 namespaced classes [wb, 2011-05-26]
 * @changes    1.0.0b78  Backwards Compatibility Break - ::reflect() now returns an associative array instead of a string [wb, 2011-05-10]
 * @changes    1.0.0b77  Fixed ::inspect() to not throw an fProgrammerException when a valid element has a `NULL` value [wb, 2011-05-10]
 * @changes    1.0.0b76  Added ::clearIdentityMap() [wb, 2011-05-09]
 * @changes    1.0.0b75  Fixed a bug where child records of a record with a non-auto-incrementing primary key would not be saved properly for a new record [wb, 2010-12-06]
 * @changes    1.0.0b74  Updated ::populate() to use the `binary` type for fRequest::get() [wb, 2010-11-30]
 * @changes    1.0.0b73  Backwards Compatibility Break - changed column set methods to treat strings of all whitespace the same as empty string and convert them to `NULL` [wb, 2010-11-29]
 * @changes    1.0.0b72  Added the new `comment` element to the reflection signature for `inspect` methods [wb, 2010-11-28]
 * @changes    1.0.0b71  Updated class to use fORM::getRelatedClass() [wb, 2010-11-24]
 * @changes    1.0.0b70  Added support for PHP 5.3 namespaced fActiveRecord classes [wb, 2010-11-11]
 * @changes    1.0.0b69  Backwards Compatibility Break - changed ::validate() to return a nested array of validation messages when there are validation errors on child records [wb-imarc+wb, 2010-10-03]
 * @changes    1.0.0b68  Added hooks to ::replicate() [wb, 2010-09-07]
 * @changes    1.0.0b67  Updated code to work with the new fORM API [wb, 2010-08-06]
 * @changes    1.0.0b66  Fixed a bug with ::store() and non-primary key auto-incrementing columns [wb, 2010-07-05]
 * @changes    1.0.0b65  Fixed bugs with ::inspect() making some `min_value` and `max_value` elements available for non-numeric types, fixed ::reflect() to list the `min_value` and `max_value` elements [wb, 2010-06-08]
 * @changes    1.0.0b64  BackwardsCompatibilityBreak - changed ::validate()'s returned messages array to have field name keys - added the option to ::validate() to remove field names from messages [wb, 2010-05-26]
 * @changes    1.0.0b63  Changed how is_subclass_of() is used to work around a bug in 5.2.x [wb, 2010-04-06]
 * @changes    1.0.0b62  Fixed a bug that could cause infinite recursion starting in v1.0.0b60 [wb, 2010-04-02]
 * @changes    1.0.0b61  Fixed issues with handling `populate` actions when working with mapped classes [wb, 2010-03-31]
 * @changes    1.0.0b60  Fixed issues with handling `associate` and `has` actions when working with mapped classes, added ::validateClass() [wb, 2010-03-30]
 * @changes    1.0.0b59  Changed an extended UTF-8 arrow character into the correct `->` [wb, 2010-03-29]
 * @changes    1.0.0b58  Fixed ::reflect() to specify the value returned from `set` methods [wb, 2010-03-15]
 * @changes    1.0.0b57  Added the `post::loadFromIdentityMap()` hook and fixed ::__construct() to always call the `post::__construct()` hook [wb, 2010-03-14]
 * @changes    1.0.0b56  Fixed `$force_cascade` in ::delete() to work even when the restricted relationship is once-removed through an unrestricted relationship [wb, 2010-03-09]
 * @changes    1.0.0b55  Fixed ::load() to that related records are cleared, requiring them to be loaded from the database [wb, 2010-03-04]
 * @changes    1.0.0b54  Fixed detection of route name for one-to-one relationships in ::delete() [wb, 2010-03-03]
 * @changes    1.0.0b53  Fixed a bug where related records with a primary key that contained a foreign key with an on update cascade clause would be deleted when changing the value of the column referenced by the foreign key [wb, 2009-12-17]
 * @changes    1.0.0b52  Backwards Compatibility Break - Added the $force_cascade parameter to ::delete() and ::store() - enabled calling ::prepare() and ::encode() for non-column get methods, added `::has{RelatedRecords}()` methods [wb, 2009-12-16]
 * @changes    1.0.0b51  Made ::changed() properly recognize that a blank string and NULL are equivalent due to the way that ::set() casts values [wb, 2009-11-14]
 * @changes    1.0.0b50  Fixed a bug with trying to load by a multi-column primary key where one of the columns was not specified [wb, 2009-11-13]
 * @changes    1.0.0b49  Fixed a bug affecting where conditions with columns that are not null but have a default value [wb, 2009-11-03]
 * @changes    1.0.0b48  Updated code for the new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
 * @changes    1.0.0b47  Changed `::associate{RelatedRecords}()`, `::link{RelatedRecords}()` and `::populate{RelatedRecords}()` to allow for method chaining [wb, 2009-10-22]
 * @changes    1.0.0b46  Changed SQL statements to use value placeholders and identifier escaping [wb, 2009-10-22]
 * @changes    1.0.0b45  Added support for `!~`, `&~`, `><` and OR comparisons to ::checkConditions(), made object handling in ::checkConditions() more robust [wb, 2009-09-21]
 * @changes    1.0.0b44  Updated code for new fValidationException API [wb, 2009-09-18]
 * @changes    1.0.0b43  Updated code for new fRecordSet API [wb, 2009-09-16]
 * @changes    1.0.0b42  Corrected a grammar bug in ::hash() [wb, 2009-09-09]
 * @changes    1.0.0b41  Fixed a bug in the last version that would cause issues with classes containing a custom class to table mapping [wb, 2009-09-01]
 * @changes    1.0.0b40  Added a check to the configuration part of ::__construct() to ensure modelled tables have primary keys [wb, 2009-08-26]
 * @changes    1.0.0b39  Changed `set{ColumnName}()` methods to return the record for method chaining, fixed a bug with loading by multi-column unique constraints, fixed a bug with ::load() [wb, 2009-08-26]
 * @changes    1.0.0b38  Updated ::changed() to do a strict comparison when at least one value is NULL [wb, 2009-08-17]
 * @changes    1.0.0b37  Changed ::__construct() to allow any Iterator object instead of just fResult [wb, 2009-08-12]
 * @changes    1.0.0b36  Fixed a bug with setting NULL values from v1.0.0b33 [wb, 2009-08-10]
 * @changes    1.0.0b35  Fixed a bug with unescaping data in ::loadFromResult() from v1.0.0b33 [wb, 2009-08-10]
 * @changes    1.0.0b34  Added the ability to compare fActiveRecord objects in ::checkConditions() [wb, 2009-08-07]
 * @changes    1.0.0b33  Performance enhancements to ::__call() and ::__construct() [wb, 2009-08-07] 
 * @changes    1.0.0b32  Changed ::delete() to remove auto-incrementing primary keys after the post::delete() hook [wb, 2009-07-29]
 * @changes    1.0.0b31  Fixed a bug with loading a record by a multi-column primary key, fixed one-to-one relationship API [wb, 2009-07-21]
 * @changes    1.0.0b30  Updated ::reflect() for new fORM::callReflectCallbacks() API [wb, 2009-07-13]
 * @changes    1.0.0b29  Updated to use new fORM::callInspectCallbacks() method [wb, 2009-07-13]
 * @changes    1.0.0b28  Fixed a bug where records would break the identity map at the end of ::store() [wb, 2009-07-09]
 * @changes    1.0.0b27  Changed ::hash() from a protected method to a static public/internal method that requires the class name for non-fActiveRecord values [wb, 2009-07-09]
 * @changes    1.0.0b26  Added ::checkConditions() from fRecordSet [wb, 2009-07-08]
 * @changes    1.0.0b25  Updated ::validate() to use new fORMValidation API, including new message search/replace functionality [wb, 2009-07-01]
 * @changes    1.0.0b24  Changed ::validate() to remove duplicate validation messages [wb, 2009-06-30]
 * @changes    1.0.0b23  Updated code for new fORMValidation::validateRelated() API [wb, 2009-06-26]
 * @changes    1.0.0b22  Added support for the $formatting parameter to encode methods on char, text and varchar columns [wb, 2009-06-19]
 * @changes    1.0.0b21  Performance tweaks and updates for fORM and fORMRelated API changes [wb, 2009-06-15]
 * @changes    1.0.0b20  Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
 * @changes    1.0.0b19  Added `list{RelatedRecords}()` methods, updated code for new fORMRelated API [wb, 2009-06-02]
 * @changes    1.0.0b18  Changed ::store() to use new fORMRelated::store() method [wb, 2009-06-02]
 * @changes    1.0.0b17  Added some missing parameter information to ::reflect() [wb, 2009-06-01]
 * @changes    1.0.0b16  Fixed bugs in ::__clone() and ::replicate() related to recursive relationships [wb-imarc, 2009-05-20]
 * @changes    1.0.0b15  Fixed an incorrect variable reference in ::store() [wb, 2009-05-06]
 * @changes    1.0.0b14  ::store() no longer tries to get an auto-incrementing ID from the database if a value was set [wb, 2009-05-02]
 * @changes    1.0.0b13  ::delete(), ::load(), ::populate() and ::store() now return the record to allow for method chaining [wb, 2009-03-23]
 * @changes    1.0.0b12  ::set() now removes commas from integers and floats to prevent validation issues [wb, 2009-03-22]
 * @changes    1.0.0b11  ::encode() no longer adds commas to floats [wb, 2009-03-22]
 * @changes    1.0.0b10  ::__wakeup() no longer registers the record as the definitive copy in the identity map [wb, 2009-03-22]
 * @changes    1.0.0b9   Changed ::__construct() to populate database default values when a non-existing record is instantiated [wb, 2009-01-12]
 * @changes    1.0.0b8   Fixed ::exists() to properly detect cases when an existing record has one or more NULL values in the primary key [wb, 2009-01-11]
 * @changes    1.0.0b7   Fixed ::__construct() to not trigger the post::__construct() hook when force-configured [wb, 2008-12-30]
 * @changes    1.0.0b6   ::__construct() now accepts an associative array matching any unique key or primary key, fixed the post::__construct() hook to be called once for each record [wb, 2008-12-26]
 * @changes    1.0.0b5   Fixed ::replicate() to use plural record names for related records [wb, 2008-12-12]
 * @changes    1.0.0b4   Added ::replicate() to allow cloning along with related records [wb, 2008-12-12]
 * @changes    1.0.0b3   Changed ::__clone() to clone objects contains in the values and cache arrays [wb, 2008-12-11]
 * @changes    1.0.0b2   Added the ::__clone() method to properly duplicate a record [wb, 2008-12-04]
 * @changes    1.0.0b    The initial implementation [wb, 2007-08-04]
 */
abstract class fActiveRecord
{
	// The following constants allow for nice looking callbacks to static methods
	const assign          = 'fActiveRecord::assign';
	const changed         = 'fActiveRecord::changed';
	const checkConditions = 'fActiveRecord::checkConditions';
	const forceConfigure  = 'fActiveRecord::forceConfigure';
	const hasOld          = 'fActiveRecord::hasOld';
	const reset           = 'fActiveRecord::reset';
	const retrieveOld     = 'fActiveRecord::retrieveOld';
	const validateClass   = 'fActiveRecord::validateClass';
	
	
	/**
	 * Caches callbacks for methods
	 * 
	 * @var array
	 */
	static protected $callback_cache = array();
	
	/**
	 * An array of flags indicating a class has been configured
	 * 
	 * @var array
	 */
	static protected $configured = array();
	
	
	/**
	 * Maps objects via their primary key
	 * 
	 * @var array
	 */
	static protected $identity_map = array();
	
	
	/**
	 * Caches method name parsings
	 * 
	 * @var array
	 */
	static protected $method_name_cache = array();
	
	
	/**
	 * Keeps track of the recursive call level of replication so we can clear the map
	 * 
	 * @var integer
	 */
	static protected $replicate_level = 0;
	
	
	/**
	 * Keeps a list of records that have been replicated
	 * 
	 * @var array
	 */
	static protected $replicate_map = array();
	
	/**
	 * Contains a list of what columns in each class need to be unescaped and what data type they are
	 * 
	 * @var array
	 */
	static protected $unescape_map = array();
	
	
	/**
	 * Sets a value to the `$values` array, preserving the old value in `$old_values`
	 *
	 * @internal
	 * 
	 * @param  array  &$values      The current values
	 * @param  array  &$old_values  The old values
	 * @param  string $column       The column to set
	 * @param  mixed  $value        The value to set
	 * @return void
	 */
	static public function assign(&$values, &$old_values, $column, $value)
	{
		if (!isset($old_values[$column])) {
			$old_values[$column] = array();
		}
		
		$old_values[$column][] = $values[$column];
		$values[$column]       = $value;	
	}
	
	
	/**
	 * Checks to see if a value has changed
	 *
	 * @internal
	 * 
	 * @param  array  &$values      The current values
	 * @param  array  &$old_values  The old values
	 * @param  string $column       The column to check
	 * @return boolean  If the value for the column specified has changed
	 */
	static public function changed(&$values, &$old_values, $column)
	{
		if (!isset($old_values[$column])) {
			return FALSE;
		}
		
		$oldest_value = $old_values[$column][0];
		$new_value    = $values[$column];
		
		// We do a strict comparison when one of the values is NULL since
		// NULL is almost always meant to be distinct from 0, FALSE, etc.
		// However, since we cast blank strings to NULL in ::set() but a blank
		// string could come out of the database, we consider them to be
		// equivalent, so we don't do a strict comparison
		if (($oldest_value === NULL && $new_value !== '') || ($new_value === NULL && $oldest_value !== '')) {
			return $oldest_value !== $new_value;	
		}
		
		return $oldest_value != $new_value;	
	}
	
	
	/**
	 * Ensures a class extends fActiveRecord
	 * 
	 * @internal
	 * 
	 * @param  string $class  The class to check
	 * @return boolean  If the class is an fActiveRecord descendant
	 */
	static public function checkClass($class)
	{
		if (isset(self::$configured[$class])) {
			return TRUE;
		}
		
		if (!is_string($class) || !$class || !class_exists($class) || !($class == 'fActiveRecord' || is_subclass_of($class, 'fActiveRecord'))) {
			return FALSE;
		}
		return TRUE;
	}
	
	
	/**
	 * Checks to see if a record matches a condition
	 * 
	 * @internal
	 * 
	 * @param  string $operator  The record to check
	 * @param  mixed  $value     The value to compare to
	 * @param  mixed $result     The result of the method call(s)
	 * @return boolean  If the comparison was successful
	 */
	static private function checkCondition($operator, $value, $result)
	{
		$was_array = is_array($value);
		if (!$was_array) { $value = array($value); }
		foreach ($value as $i => $_value) {
			if (is_object($_value)) {
				if ($_value instanceof fActiveRecord) {
					continue;
				}
				if (method_exists($_value, '__toString')) {
					$value[$i] = $_value->__toString();
				}	
			}	
		}
		if (!$was_array) { $value = $value[0]; }
		
		$was_array = is_array($result);
		if (!$was_array) { $result = array($result); }
		foreach ($result as $i => $_result) {
			if (is_object($_result)) {
				if ($_result instanceof fActiveRecord) {
					continue;
				}
				if (method_exists($_result, '__toString')) {
					$result[$i] = $_result->__toString();
				}	
			}	
		}
		if (!$was_array) { $result = $result[0]; }
		
		if ($operator == '~' && !is_array($value) && is_array($result)) {
			$value = fORMDatabase::parseSearchTerms($value, TRUE);
		}

		if (in_array($operator, array('~', '&~', '!~', '^~', '$~'))) {
			settype($value, 'array');
			settype($result, 'array');
		}

		switch ($operator) {
			case '&~':
				foreach ($value as $_value) {
					if (fUTF8::ipos($result[0], $_value) === FALSE) {
						return FALSE;
					}
				}
				break;

			case '~':
				
				// Handles fuzzy search on multiple method calls
				if (count($result) > 1) {
					foreach ($value as $_value) {
						$found = FALSE;
						foreach ($result as $_result) {
							if (fUTF8::ipos($_result, $_value) !== FALSE) {
								$found = TRUE;
							}
						}
						if (!$found) {
							return FALSE;
						}	
					}
					break;
				}

				// No break exists since a ~ on a single method call acts
				// similar to the other LIKE operators

			case '!~':
			case '^~':
			case '$~':
				if ($operator == '$~') {
					$result_len = fUTF8::len($result[0]);
				}

				foreach ($value as $_value) {
					$pos = fUTF8::ipos($result[0], $_value);
					if ($operator == '^~' && $pos === 0) {
						return TRUE;
					} elseif ($operator == '$~' && $pos === $result_len - fUTF8::len($_value)) {
						return TRUE;
					} elseif ($pos !== FALSE) {
						return $operator != '!~';
					}
				}

				if ($operator != '!~') {
					return FALSE;
				}
				break;
			
			case '=':
				if ($value instanceof fActiveRecord && $result instanceof fActiveRecord) {
					if (get_class($value) != get_class($result) || !$value->exists() || !$result->exists() || self::hash($value) != self::hash($result)) {
						return FALSE;
					}
					
				} elseif (is_array($value) && !in_array($result, $value)) {
					return FALSE;
						
				} elseif (!is_array($value) && $result != $value) {
					return FALSE;	
				}
				break;
				
			case '!':
				if ($value instanceof fActiveRecord && $result instanceof fActiveRecord) {
					if (get_class($value) == get_class($result) && $value->exists() && $result->exists() && self::hash($value) == self::hash($result)) {
						return FALSE;
					}
					
				} elseif (is_array($value) && in_array($result, $value)) {
					return FALSE;	
					
				} elseif (!is_array($value) && $result == $value) {
					return FALSE;	
				}
				break;
			
			case '<':
				if ($result >= $value) {
					return FALSE;	
				}
				break;
			
			case '<=':
				if ($result > $value) {
					return FALSE;	
				}
				break;
			
			case '>':
				if ($result <= $value) {
					return FALSE;	
				}
				break;
			
			case '>=':
				if ($result < $value) {
					return FALSE;	
				}
				break;
		}
		
		return TRUE;		
	}
	
	
	/**
	 * Checks to see if a record matches all of the conditions
	 * 
	 * @internal
	 * 
	 * @param  fActiveRecord $record      The record to check
	 * @param  array         $conditions  The conditions to check - see fRecordSet::filter() for format details
	 * @return boolean  If the record meets all conditions
	 */
	static public function checkConditions($record, $conditions)
	{
		foreach ($conditions as $method => $value) {
			
			// Split the operator off of the end of the method name
			if (in_array(substr($method, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '^~', '$~', '><'))) {
				$operator = strtr(
					substr($method, -2),
					array(
						'<>' => '!',
						'!=' => '!'
					)
				);
				$method   = substr($method, 0, -2);
			} else {
				$operator = substr($method, -1);
				$method   = substr($method, 0, -1);
			}
			
			if (preg_match('#(?<!\|)\|(?!\|)#', $method)) {
				
				$methods   = explode('|', $method);
				$values    = $value;
				$operators = array();
				
				foreach ($methods as &$_method) {
					if (in_array(substr($_method, -2), array('<=', '>=', '!=', '<>', '!~', '&~', '^~', '$~', '><'))) {
						$operators[] = strtr(
							substr($_method, -2),
							array(
								'<>' => '!',
								'!=' => '!'
							)
						);
						$_method     = substr($_method, 0, -2);
					} elseif (!ctype_alnum(substr($_method, -1))) {
						$operators[] = substr($_method, -1);
						$_method     = substr($_method, 0, -1);
					}
				}
				$operators[] = $operator;
				
				
				if (sizeof($operators) == 1) {
				
					// Handle fuzzy searches
					if ($operator == '~') {
					
						$results = array();
						foreach ($methods as $method) {
							$results[] = $record->$method();	
						}
						if (!self::checkCondition($operator, $value, $results)) {
							return FALSE;	
						}
					
					// Handle intersection
					} elseif ($operator == '><') {
						
						if (sizeof($methods) != 2 || sizeof($values) != 2) {
							throw new fProgrammerException(
								'The intersection operator, %s, requires exactly two methods and two values',
								$operator
							);	
						}
									
						$results    = array();
						$results[0] = $record->{$methods[0]}();
						$results[1] = $record->{$methods[1]}();
						
						if ($results[1] === NULL && $values[1] === NULL) {
							if (!self::checkCondition('=', $values[0], $results[0])) {
								return FALSE;
							}
							
							
						} else {
							
							$starts_between_values = FALSE;
							$overlaps_value_1      = FALSE;
							
							if ($values[1] !== NULL) {
								$start_lt_value_1      = self::checkCondition('<', $values[0], $results[0]);
								$start_gt_value_2      = self::checkCondition('>', $values[1], $results[0]);
								$starts_between_values = !$start_lt_value_1 && !$start_gt_value_2;
							}
							if ($results[1] !== NULL) {
								$start_gt_value_1 = self::checkCondition('>', $values[0], $results[0]);
								$end_lt_value_1   = self::checkCondition('<', $values[0], $results[1]);
								$overlaps_value_1 = !$start_gt_value_1 && !$end_lt_value_1;
							}
							
							if (!$starts_between_values && !$overlaps_value_1) {
								return FALSE;
							}
						}
					
					} else {
						throw new fProgrammerException(
							'An invalid comparison operator, %s, was specified for multiple columns',
							$operator
						);
					}
					
				// Handle OR conditions
				} else {
					
					if (sizeof($methods) != sizeof($values)) {
						throw new fProgrammerException(
							'When performing an %1$s comparison there must be an equal number of methods and values, however there are not',
							'OR',
							sizeof($methods),
							sizeof($values)
						);
					}
					
					if (sizeof($methods) != sizeof($operators)) {
						throw new fProgrammerException(
							'When performing an %s comparison there must be a comparison operator for each column, however one or more is missing',
							'OR'
						);
					}
					
					$results    = array();
					$iterations = sizeof($methods);
					for ($i=0; $i<$iterations; $i++) {
						$results[] = self::checkCondition($operators[$i], $values[$i], $record->{$methods[$i]}());
					}
					
					if (!array_filter($results)) {
						return FALSE;	
					}
					
				}
				
			// Single method comparisons	
			} else {
				$result = $record->$method();
				if (!self::checkCondition($operator, $value, $result)) {
					return FALSE;	
				}
			}	
		}
		
		return TRUE;
	}


	/**
	 * Clears the identity map
	 * 
	 * @return void
	 */
	static public function clearIdentityMap()
	{
		self::$identity_map = array();
	}
	
	
	/**
	 * Composes text using fText if loaded
	 * 
	 * @param  string  $message    The message to compose
	 * @param  mixed   $component  A string or number to insert into the message
	 * @param  mixed   ...
	 * @return string  The composed and possible translated message
	 */
	static protected function compose($message)
	{
		$args = array_slice(func_get_args(), 1);
		
		if (class_exists('fText', FALSE)) {
			return call_user_func_array(
				array('fText', 'compose'),
				array($message, $args)
			);
		} else {
			return vsprintf($message, $args);
		}
	}
	
	
	/**
	 * Takes information from a method call and determines the subject, route and if subject was plural
	 * 
	 * @param string $class    The class the method was called on
	 * @param string $subject  An underscore_notation subject - either a singular or plural class name
	 * @param string $route    The route to the subject
	 * @return array  An array with the structure: array(0 => $subject, 1 => $route, 2 => $plural)
	 */
	static private function determineSubject($class, $subject, $route)
	{
		$schema  = fORMSchema::retrieve($class);
		$table   = fORM::tablize($class);
		$type    = '*-to-many';
		$plural  = FALSE;
		
		// one-to-many relationships need to use plural forms
		$singular_form = fGrammar::singularize($subject, TRUE);
		if ($singular_form && fORM::isClassMappedToTable($singular_form)) {
			$subject = $singular_form;
			$plural  = TRUE;
			
		} elseif (!fORM::isClassMappedToTable($subject) && in_array(fGrammar::underscorize($subject), $schema->getTables())) {
			$subject = fGrammar::singularize($subject);
			$plural  = TRUE;
		}
		
		$related_table = fORM::tablize($subject);
		$one_to_one    = fORMSchema::isOneToOne($schema, $table, $related_table, $route);
		if ($one_to_one) {
			$type = 'one-to-one';
		}
		if (($one_to_one && $plural) || (!$plural && !$one_to_one)) {
			throw new fProgrammerException(
				'The table %1$s is not in a %2$srelationship with the table %3$s',
				$table,
				$type,
				$related_table
			); 
		}
		
		$route = fORMSchema::getRouteName($schema, $table, $related_table, $route, $type);
		
		return array($subject, $route, $plural);
	}
	
	
	/**
	 * Ensures that ::configure() has been called for the class
	 *
	 * @internal
	 * 
	 * @param  string $class  The class to configure
	 * @return void
	 */
	static public function forceConfigure($class)
	{
		if (isset(self::$configured[$class])) {
			return;	
		}
		new $class();
	}
	
	
	/**
	 * Takes a row of data or a primary key and makes a hash from the primary key
	 * 
	 * @internal
	 * 
	 * @param  fActiveRecord|array|string|int $record   An fActiveRecord object, an array of the records data, an array of primary key data or a scalar primary key value
	 * @param  string                         $class    The class name, if $record isn't an fActiveRecord
	 * @return string|NULL  A hash of the record's primary key value or NULL if the record doesn't exist yet
	 */
	static public function hash($record, $class=NULL)
	{
		if ($record instanceof fActiveRecord && !$record->exists()) {
			return NULL;	
		}
		
		if ($class === NULL) {
			if (!$record instanceof fActiveRecord) {
				throw new fProgrammerException(
					'The class of the record must be provided if the record specified is not an instance of fActiveRecord'
				);
			}
			$class = get_class($record);	
		}
		
		$schema     = fORMSchema::retrieve($class);
		$table      = fORM::tablize($class);
		$pk_columns = $schema->getKeys($table, 'primary');
		
		// Build an array of just the primary key data
		$pk_data = array();
		foreach ($pk_columns as $pk_column) {
			if ($record instanceof fActiveRecord) {
				$value = (self::hasOld($record->old_values, $pk_column)) ? self::retrieveOld($record->old_values, $pk_column) : $record->values[$pk_column];
			
			} elseif (is_array($record)) {
				$value = $record[$pk_column];
			
			} else {
				$value = $record;	
			}
			
			$pk_data[$pk_column] = fORM::scalarize(
				$class,
				$pk_column,
				$value
			);
			
			if (is_numeric($pk_data[$pk_column]) || is_object($pk_data[$pk_column])) {
				$pk_data[$pk_column] = (string) $pk_data[$pk_column];	
			}
		}
		
		return md5(serialize($pk_data));
	}
	
	
	/**
	 * Checks to see if an old value exists for a column 
	 *
	 * @internal
	 * 
	 * @param  array  &$old_values  The old values
	 * @param  string $column       The column to set
	 * @return boolean  If an old value for that column exists
	 */
	static public function hasOld(&$old_values, $column)
	{
		return array_key_exists($column, $old_values);
	}
	
	
	/**
	 * Resets the configuration of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		self::$callback_cache    = array();
		self::$configured        = array();
		self::$identity_map      = array();
		self::$method_name_cache = array();
		self::$unescape_map      = array();
	}
	
	
	/**
	 * Retrieves the oldest value for a column or all old values
	 *
	 * @internal
	 * 
	 * @param  array   &$old_values  The old values
	 * @param  string  $column       The column to get
	 * @param  mixed   $default      The default value to return if no value exists
	 * @param  boolean $return_all   Return the array of all old values for this column instead of just the oldest
	 * @return mixed  The old value for the column
	 */
	static public function retrieveOld(&$old_values, $column, $default=NULL, $return_all=FALSE)
	{
		if (!isset($old_values[$column])) {
			return $default;	
		}
		
		if ($return_all === TRUE) {
			return $old_values[$column];	
		}
		
		return $old_values[$column][0];
	}
	
	
	/**
	 * Ensures a class extends fActiveRecord
	 * 
	 * @internal
	 * 
	 * @param  string $class  The class to verify
	 * @return void
	 */
	static public function validateClass($class)
	{
		if (isset(self::$configured[$class])) {
			return TRUE;
		}
		
		if (!self::checkClass($class)) {
			throw new fProgrammerException(
				'The class specified, %1$s, does not appear to be a valid %2$s class',
				$class,
				'fActiveRecord'
			);
		}
	}
	
	
	/**
	 * A data store for caching data related to a record, the structure of this is completely up to the developer using it
	 * 
	 * @var array
	 */
	protected $cache = array();
	
	/**
	 * The old values for this record
	 * 
	 * Column names are the keys, but a column key will only be present if a
	 * value has changed. The value associated with each key is an array of
	 * old values with the first entry being the oldest value. The static 
	 * methods ::assign(), ::changed(), ::hasOld() and ::retrieveOld() are the
	 * best way to interact with this array.
	 * 
	 * @var array
	 */
	protected $old_values = array();
	
	/**
	 * Records that are related to the current record via some relationship
	 * 
	 * This array is used to cache related records so that a database query
	 * is not required each time related records are accessed. The fORMRelated
	 * class handles most of the interaction with this array.
	 * 
	 * @var array
	 */
	protected $related_records = array();
	
	/**
	 * The values for this record
	 * 
	 * This array always contains every column in the database table as a key
	 * with the value being the current value. 
	 * 
	 * @var array
	 */
	protected $values = array();
	
	
	/**
	 * Handles all method calls for columns, related records and hook callbacks
	 * 
	 * Dynamically handles `get`, `set`, `prepare`, `encode` and `inspect`
	 * methods for each column in this record. Method names are in the form
	 * `verbColumName()`.
	 * 
	 * This method also handles `associate`, `build`, `count`, `has`, and `link`
	 * verbs for records in many-to-many relationships; `build`, `count`, `has`
	 * and `populate` verbs for all related records in one-to-many relationships
	 * and `create`, `has` and `populate` verbs for all related records in
	 * one-to-one relationships, and the `create` verb for all related records
	 * in many-to-one relationships.
	 * 
	 * Method callbacks registered through fORM::registerActiveRecordMethod()
	 * will be delegated via this method.
	 * 
	 * @param  string $method_name  The name of the method called
	 * @param  array  $parameters   The parameters passed
	 * @return mixed  The value returned by the method called
	 */
	public function __call($method_name, $parameters)
	{
		$class = get_class($this);
		
		if (!isset(self::$callback_cache[$class][$method_name])) {
			if (!isset(self::$callback_cache[$class])) {
				self::$callback_cache[$class] = array();
			}	
			$callback = fORM::getActiveRecordMethod($class, $method_name);
			self::$callback_cache[$class][$method_name] = $callback ? $callback : FALSE;
		}
		
		if ($callback = self::$callback_cache[$class][$method_name]) {
			return call_user_func_array(
				$callback,
				array(
					$this,
					&$this->values,
					&$this->old_values,
					&$this->related_records,
					&$this->cache,
					$method_name,
					$parameters
				)
			);
		}
		
		if (!isset(self::$method_name_cache[$method_name])) {
			list ($action, $subject) = fORM::parseMethod($method_name);
			if (in_array($action, array('get', 'encode', 'prepare', 'inspect', 'set'))) {
				$subject = fGrammar::underscorize($subject);
			} else {
				if (in_array($action, array('build', 'count', 'inject', 'link', 'list', 'tally'))) {
					$subject = fGrammar::singularize($subject);
				}
				$subject = fORM::getRelatedClass($class, $subject);
			}
			self::$method_name_cache[$method_name] = array(
				'action'  => $action,
				'subject' => $subject
			);	
		} else {
			$action  = self::$method_name_cache[$method_name]['action'];
			$subject = self::$method_name_cache[$method_name]['subject'];	
		}
		
		switch ($action) {
			
			// Value methods
			case 'get':
				return $this->get($subject);
				
			case 'encode':
				if (isset($parameters[0])) {
					return $this->encode($subject, $parameters[0]);
				}
				return $this->encode($subject);
			
			case 'prepare':
				if (isset($parameters[0])) {
					return $this->prepare($subject, $parameters[0]);
				}
				return $this->prepare($subject);
			
			case 'inspect':
				if (isset($parameters[0])) {
					return $this->inspect($subject, $parameters[0]);
				}
				return $this->inspect($subject);
			
			case 'set':
				if (sizeof($parameters) < 1) {
					throw new fProgrammerException(
						'The method, %s(), requires at least one parameter',
						$method_name
					);
				}
				return $this->set($subject, $parameters[0]);
			
			// Related data methods
			case 'associate':
				if (sizeof($parameters) < 1) {
					throw new fProgrammerException(
						'The method, %s(), requires at least one parameter',
						$method_name
					);
				}
				
				$records = $parameters[0];
				$route   = isset($parameters[1]) ? $parameters[1] : NULL;
				
				list ($subject, $route, $plural) = self::determineSubject($class, $subject, $route);
				
				if ($plural) {
					fORMRelated::associateRecords($class, $this->related_records, $subject, $records, $route);
				} else {
					fORMRelated::associateRecord($class, $this->related_records, $subject, $records, $route);
				}
				return $this;
			
			case 'build':
				if (isset($parameters[0])) {
					return fORMRelated::buildRecords($class, $this->values, $this->related_records, $subject, $parameters[0]);
				}
				return fORMRelated::buildRecords($class, $this->values, $this->related_records, $subject);
			
			case 'count':
				if (isset($parameters[0])) {
					return fORMRelated::countRecords($class, $this->values, $this->related_records, $subject, $parameters[0]);
				}
				return fORMRelated::countRecords($class, $this->values, $this->related_records, $subject);
			
			case 'create':
				if (isset($parameters[0])) {
					return fORMRelated::createRecord($class, $this->values, $this->related_records, $subject, $parameters[0]);
				}
				return fORMRelated::createRecord($class, $this->values, $this->related_records, $subject);
				
			case 'has':
				$route = isset($parameters[0]) ? $parameters[0] : NULL;
				
				list ($subject, $route, ) = self::determineSubject($class, $subject, $route);
				
				return fORMRelated::hasRecords($class, $this->values, $this->related_records, $subject, $route);
			 
			case 'inject':
				if (sizeof($parameters) < 1) {
					throw new fProgrammerException(
						'The method, %s(), requires at least one parameter',
						$method_name
					);
				}
				
				if (isset($parameters[1])) {
					return fORMRelated::setRecordSet($class, $this->related_records, $subject, $parameters[0], $parameters[1]);
				}
				return fORMRelated::setRecordSet($class, $this->related_records, $subject, $parameters[0]);

			case 'link':
				if (isset($parameters[0])) {
					fORMRelated::linkRecords($class, $this->related_records, $subject, $parameters[0]);
				} else {
					fORMRelated::linkRecords($class, $this->related_records, $subject);
				}
				return $this;
			
			case 'list':
				if (isset($parameters[0])) {
					return fORMRelated::getPrimaryKeys($class, $this->values, $this->related_records, $subject, $parameters[0]);
				}
				return fORMRelated::getPrimaryKeys($class, $this->values, $this->related_records, $subject);
			
			case 'populate':
				$route = isset($parameters[0]) ? $parameters[0] : NULL;
				
				list ($subject, $route, ) = self::determineSubject($class, $subject, $route);
				
				fORMRelated::populateRecords($class, $this->related_records, $subject, $route);
				return $this;
			
			case 'tally':
				if (sizeof($parameters) < 1) {
					throw new fProgrammerException(
						'The method, %s(), requires at least one parameter',
						$method_name
					);
				}
				
				if (isset($parameters[1])) {
					return fORMRelated::setCount($class, $this->related_records, $subject, $parameters[0], $parameters[1]);
				}
				return fORMRelated::setCount($class, $this->related_records, $subject, $parameters[0]);
			
			// Error handler
			default:
				throw new fProgrammerException(
					'Unknown method, %s(), called',
					$method_name
				);
		}
	}
	
	
	/**
	 * Creates a clone of a record
	 * 
	 * If the record has an auto incrementing primary key, the primary key will
	 * be erased in the clone. If the primary key is not auto incrementing,
	 * the primary key will be left as-is in the clone. In either situation the
	 * clone will return `FALSE` from the ::exists() method until ::store() is
	 * called.
	 * 
	 * @internal
	 * 
	 * @return fActiveRecord
	 */
	public function __clone()
	{
		$class = get_class($this);
		
		// Copy values and cache, making sure objects are cloned to prevent reference issues
		$temp_values  = $this->values;
		$new_values   = array();
		$this->values =& $new_values;
		foreach ($temp_values as $column => $value) {
			$this->values[$column] = fORM::replicate($class, $column, $value);
		}
		
		$temp_cache  = $this->cache;
		$new_cache   = array();
		$this->cache =& $new_cache;
		foreach ($temp_cache as $key => $value) {
			if (is_object($value)) {
				$this->cache[$key] = clone $value;
			} else {
				$this->cache[$key] = $value;
			}		
		}
		
		// Related records are purged
		$new_related_records   = array();
		$this->related_records =& $new_related_records;
		
		// Old values are changed to look like the record is non-existant
		$new_old_values   = array();
		$this->old_values =& $new_old_values;
		
		foreach (array_keys($this->values) as $key) {
			$this->old_values[$key] = array(NULL);
		}
		
		// If we have a single auto incrementing primary key, remove the value
		$schema     = fORMSchema::retrieve($class);
		$table      = fORM::tablize($class);
		$pk_columns = $schema->getKeys($table, 'primary');
		
		if (sizeof($pk_columns) == 1 && $schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) {
			$this->values[$pk_columns[0]] = NULL;
			unset($this->old_values[$pk_columns[0]]);
		}		
	}
	
	
	/**
	 * Creates a new record or loads one from the database - if a primary key or unique key is provided the record will be loaded
	 * 
	 * @throws fNotFoundException  When the record specified by `$key` can not be found in the database
	 * 
	 * @param  mixed $key  The primary key or unique key value(s) - single column primary keys will accept a scalar value, all others must be an associative array of `(string) {column} => (mixed) {value}`
	 * @return fActiveRecord
	 */
	public function __construct($key=NULL)
	{
		$class  = get_class($this);
		$schema = fORMSchema::retrieve($class);
		
		// If the features of this class haven't been set yet, do it
		if (!isset(self::$configured[$class])) {
			self::$configured[$class] = TRUE;
			$this->configure();
			
			$table = fORM::tablize($class);
			if (!$schema->getKeys($table, 'primary')) {
				throw new fProgrammerException(
					'The database table %1$s (being modelled by the class %2$s) does not appear to have a primary key defined. %3$s and %4$s will not work properly without a primary key.',
					$table,
					$class,
					'fActiveRecord',
					'fRecordSet'
				);	
			}
			
			// If the configuration was forced, prevent the post::__construct() hook from
			// being triggered since it is not really a real record instantiation
			$trace = array_slice(debug_backtrace(), 0, 2);
			
			$is_forced = sizeof($trace) == 2;
			$is_forced = $is_forced && $trace[1]['function'] == 'forceConfigure';
			$is_forced = $is_forced && isset($trace[1]['class']);
			$is_forced = $is_forced && $trace[1]['type'] == '::';
			$is_forced = $is_forced && in_array($trace[1]['class'], array('fActiveRecord', $class));
			
			if ($is_forced) {
				return;	
			}
		}
		
		if (!isset(self::$callback_cache[$class]['__construct'])) {
			if (!isset(self::$callback_cache[$class])) {
				self::$callback_cache[$class] = array();
			}
			$callback = fORM::getActiveRecordMethod($class, '__construct');
			self::$callback_cache[$class]['__construct'] = $callback ? $callback : FALSE;	
		}
		if ($callback = self::$callback_cache[$class]['__construct']) {
			return $this->__call($callback);
		}
		
		// Handle loading by a result object passed via the fRecordSet class
		if ($key instanceof Iterator) {
			
			$this->loadFromResult($key);
		
		// Handle loading an object from the database
		} elseif ($key !== NULL) {
			
			$table      = fORM::tablize($class);
			$pk_columns = $schema->getKeys($table, 'primary');
			
			// If the primary key does not look properly formatted, check to see if it is a UNIQUE key
			$is_unique_key = FALSE;
			if (is_array($key) && (sizeof($pk_columns) == 1 || array_diff(array_keys($key), $pk_columns))) {
				$unique_keys = $schema->getKeys($table, 'unique');
				$key_keys    = array_keys($key);
				foreach ($unique_keys as $unique_key) {
					if (!array_diff($key_keys, $unique_key)) {
						$is_unique_key = TRUE;
					}
				}	
			}
			
			$wrong_keys = is_array($key) && (count($key) != count($pk_columns) || array_diff(array_keys($key), $pk_columns));
			$wrong_type = !is_array($key) && (sizeof($pk_columns) != 1 || !is_scalar($key));
			
			// If we didn't find a UNIQUE key and primary key doesn't look right we fail
			if (!$is_unique_key && ($wrong_keys || $wrong_type)) {
				throw new fProgrammerException(
					'An invalidly formatted primary or unique key was passed to this %s object',
					fORM::getRecordName($class)
				);
			}
			
			if ($is_unique_key) {
				
				$result = $this->fetchResultFromUniqueKey($key);
				$this->loadFromResult($result);
				
			} else {
				
				$hash = self::hash($key, $class);
				if (!$this->loadFromIdentityMap($key, $hash)) {
					// Assign the primary key values for loading
					if (is_array($key)) {
						foreach ($pk_columns as $pk_column) {
							$this->values[$pk_column] = $key[$pk_column];
						}
					} else {
						$this->values[$pk_columns[0]] = $key;
					}
					
					$this->load();
				}
			}
			
		// Create an empty array for new objects
		} else {
			$column_info = $schema->getColumnInfo(fORM::tablize($class));
			foreach ($column_info as $column => $info) {
				$this->values[$column] = NULL;
				if ($info['default'] !== NULL) {
					self::assign(
						$this->values,
						$this->old_values,
						$column,
						fORM::objectify($class, $column, $info['default'])
					);	
				}
			}
		}
		
		fORM::callHookCallbacks(
			$this,
			'post::__construct()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
	}
	
	
	/**
	 * All requests that hit this method should be requests for callbacks
	 * 
	 * @internal
	 * 
	 * @param  string $method  The method to create a callback for
	 * @return callback  The callback for the method requested
	 */
	public function __get($method)
	{
		return array($this, $method);		
	}
	
	
	/**
	 * Configure itself when coming out of the session. Records from the session are NOT hooked into the identity map.
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	public function __wakeup()
	{
		$class = get_class($this);
		
		if (!isset(self::$configured[$class])) {
			$this->configure();
			self::$configured[$class] = TRUE;
		}		
	}
	
	
	/**
	 * Allows the programmer to set features for the class
	 * 
	 * This method is only called once per page load for each class.
	 * 
	 * @return void
	 */
	protected function configure()
	{
	}
	
	
	/**
	 * Creates the fDatabase::translatedQuery() insert statement params
	 *
	 * @return array  The parameters for an fDatabase::translatedQuery() SQL insert statement
	 */
	protected function constructInsertParams()
	{
		$columns = array();
		$values  = array();
		
		$column_placeholders = array();
		$value_placeholders  = array();
		
		$class       = get_class($this);
		$schema      = fORMSchema::retrieve($class);
		$table       = fORM::tablize($class);
		$column_info = $schema->getColumnInfo($table);
		foreach ($column_info as $column => $info) {
			if ($schema->getColumnInfo($table, $column, 'auto_increment') && $schema->getColumnInfo($table, $column, 'not_null') && $this->values[$column] === NULL) {
				continue;
			}
			
			$value = fORM::scalarize($class, $column, $this->values[$column]);
			if ($value === NULL && $info['not_null'] && $info['default'] !== NULL) {
				$value = $info['default'];	
			}
			
			$columns[] = $column;
			$values[]  = $value;
			
			$column_placeholders[] = '%r';
			$value_placeholders[]  = $info['placeholder'];
		}
		
		$sql    = 'INSERT INTO %r (' . join(', ', $column_placeholders) . ') VALUES (' . join(', ', $value_placeholders) . ')';
		$params = array($sql, $table);
		$params = array_merge($params, $columns);
		$params = array_merge($params, $values);
		
		return $params;	
	}
	
	
	/**
	 * Creates the fDatabase::translatedQuery() update statement params
	 *
	 * @return array  The parameters for an fDatabase::translatedQuery() SQL update statement
	 */
	protected function constructUpdateParams()
	{
		$class       = get_class($this);
		$schema      = fORMSchema::retrieve($class);
		
		$table       = fORM::tablize($class);
		$column_info = $schema->getColumnInfo($table);
		
		$assignments = array();
		$params      = array($table);
			
		foreach ($column_info as $column => $info) {
			if ($info['auto_increment'] && !fActiveRecord::changed($this->values, $this->old_values, $column) && count($column_info) > 1) {
				continue;
			}
			
			$assignments[] = '%r = ' . $info['placeholder'];
			
			$value = fORM::scalarize($class, $column, $this->values[$column]);
			if ($value === NULL && $info['not_null'] && $info['default'] !== NULL) {
				$value = $info['default'];	
			}
			
			$params[] = $column;
			$params[] = $value;
		}
		
		$sql = 'UPDATE %r SET ' . join(', ', $assignments) . ' WHERE ';
		array_unshift($params, $sql);
		
		return fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values);
	}
	
	
	/**
	 * Deletes a record from the database, but does not destroy the object
	 * 
	 * This method will start a database transaction if one is not already active.
	 * 
	 * @param  boolean $force_cascade  When TRUE, this will cause all child objects to be deleted, even if the ON DELETE clause is RESTRICT or NO ACTION
	 * @return fActiveRecord  The record object, to allow for method chaining
	 */
	public function delete($force_cascade=FALSE)
	{
		// This flag prevents recursive relationships, such as one-to-one
		// relationships, from creating infinite loops
		if (!empty($this->cache['fActiveRecord::delete()::being_deleted'])) {
			return;	
		}
		
		$class = get_class($this);
		
		if (fORM::getActiveRecordMethod($class, 'delete')) {
			return $this->__call('delete', array());
		}
		
		if (!$this->exists()) {
			throw new fProgrammerException(
				'This %s object does not yet exist in the database, and thus can not be deleted',
				fORM::getRecordName($class)
			);
		}
		
		$db     = fORMDatabase::retrieve($class, 'write');
		$schema = fORMSchema::retrieve($class);
		
		fORM::callHookCallbacks(
			$this,
			'pre::delete()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		$table = fORM::tablize($class);
		
		$inside_db_transaction = $db->isInsideTransaction();
		
		try {
			
			if (!$inside_db_transaction) {
				$db->translatedQuery('BEGIN');
			}
			
			fORM::callHookCallbacks(
				$this,
				'post-begin::delete()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			// Check to ensure no foreign dependencies prevent deletion
			$one_to_one_relationships   = $schema->getRelationships($table, 'one-to-one');
			$one_to_many_relationships  = $schema->getRelationships($table, 'one-to-many');
			$many_to_many_relationships = $schema->getRelationships($table, 'many-to-many');
			
			$relationships = array_merge($one_to_one_relationships, $one_to_many_relationships, $many_to_many_relationships);
			$records_sets_to_delete = array();
			
			$restriction_messages = array();
			
			$this->cache['fActiveRecord::delete()::being_deleted'] = TRUE;
			
			foreach ($relationships as $relationship) {
				
				// Figure out how to check for related records
				if (isset($relationship['join_table'])) {
					$type = 'many-to-many';
				} else {
					$type = in_array($relationship, $one_to_one_relationships) ? 'one-to-one' : 'one-to-many';
				}
				$route = fORMSchema::getRouteNameFromRelationship($type, $relationship);
				
				$related_class = fORM::classize($relationship['related_table']);
				
				if ($type == 'one-to-one') {
					$method         = 'create' . $related_class;
					$related_record = $this->$method($route);
					if (!$related_record->exists()) {
						continue;
					} 
					
				} else {
					$method     = 'build' . fGrammar::pluralize($related_class);
					$record_set = $this->$method($route);
					if (!$record_set->count()) {
						continue;
					}
					
					if ($type == 'one-to-many' && $relationship['on_delete'] == 'cascade') {
						$records_sets_to_delete[] = $record_set;
					}
				}
				
				// If we are focing the cascade we have to delete child records and join table entries before this record
				if ($force_cascade) {
					
					if ($type == 'one-to-one') {
						$related_record->delete($force_cascade);
						
					// For one-to-many we explicitly delete all of the records
					} elseif ($type == 'one-to-many') {
						foreach ($record_set as $record) {
							if ($record->exists()) {
								$record->delete($force_cascade);
							}
						}
					
					// For many-to-many relationships we explicitly delete the join table entries
					} elseif ($type == 'many-to-many') {
						$join_column_placeholder = $schema->getColumnInfo($relationship['join_table'], $relationship['join_column'], 'placeholder');
						$column_get_method       = 'get' . fGrammar::camelize($relationship['column'], TRUE);
						
						$db->translatedQuery(
							$db->escape(
								'DELETE FROM %r WHERE %r = ',
								$relationship['join_table'],
								$relationship['join_column']
							) . $join_column_placeholder,
							$this->$column_get_method()
						);		
					}
				
				// Otherwise we have a restriction and we can to create a nice error message for the user
				} elseif ($relationship['on_delete'] == 'restrict' || $relationship['on_delete'] == 'no_action') {
					
					$related_class_name  = fORM::classize($relationship['related_table']);
					$related_class_name  = fORM::getRelatedClass($class, $related_class_name);
					$related_record_name = fORM::getRecordName($related_class_name);
					
					if ($type == 'one-to-one') {
						$restriction_messages[] = self::compose("A %s references it", $related_record_name);
					} else {
						$related_record_name = fGrammar::pluralize($related_record_name);
						$restriction_messages[] = self::compose("One or more %s references it", $related_record_name);
					}
				}
			}
			
			if ($restriction_messages) {
				throw new fValidationException(
					self::compose('This %s can not be deleted because:', fORM::getRecordName($class)),
					$restriction_messages
				);
			}
			
			
			// Delete this record
			$params = array('DELETE FROM %r WHERE ', $table);
			$params = fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values);
			
			$result = call_user_func_array($db->translatedQuery, $params);
			
			
			// Delete related records to ensure any PHP-level cleanup is done
			foreach ($records_sets_to_delete as $record_set) {
				foreach ($record_set as $record) {
					if ($record->exists()) {
						$record->delete($force_cascade);
					}
				}
			}
			
			unset($this->cache['fActiveRecord::delete()::being_deleted']);
			
			fORM::callHookCallbacks(
				$this,
				'pre-commit::delete()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			if (!$inside_db_transaction) {
				$db->translatedQuery('COMMIT');
			}
			
			fORM::callHookCallbacks(
				$this,
				'post-commit::delete()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
		} catch (fException $e) {
			
			if (!$inside_db_transaction) {
				$db->translatedQuery('ROLLBACK');
			}
			
			fORM::callHookCallbacks(
				$this,
				'post-rollback::delete()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			// Check to see if the validation exception came from a related record, and fix the message
			if ($e instanceof fValidationException) {
				$message = $e->getMessage();
				$search  = self::compose('This %s can not be deleted because:', fORM::getRecordName($class));
				if (stripos($message, $search) === FALSE) {
					$regex       = self::compose('This %s can not be deleted because:', '__');
					$regex_parts = explode('__', $regex);
					$regex       = '#(' . preg_quote($regex_parts[0], '#') . ').*?(' . preg_quote($regex_parts[0], '#') . ')#';
					
					$message = preg_replace($regex, '\1' . strtr(fORM::getRecordName($class), array('\\' => '\\\\', '$' => '\\$')) . '\2', $message);
					
					$find          = self::compose("One or more %s references it", '__');
					$find_parts    = explode('__', $find);
					$find_regex    = '#' . preg_quote($find_parts[0], '#') . '(.*?)' . preg_quote($find_parts[1], '#') . '#';
					
					$replace       = self::compose("One or more %s indirectly references it", '__');
					$replace_parts = explode('__', $replace);
					$replace_regex = strtr($replace_parts[0], array('\\' => '\\\\', '$' => '\\$')) . '\1' . strtr($replace_parts[1], array('\\' => '\\\\', '$' => '\\$'));
					
					$message = preg_replace($find_regex, $replace_regex, $regex);
					throw new fValidationException($message);
				}
			}
			
			throw $e;
		}
		
		fORM::callHookCallbacks(
			$this,
			'post::delete()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		// If we just deleted an object that has an auto-incrementing primary key,
		// lets delete that value from the object since it is no longer valid
		$pk_columns  = $schema->getKeys($table, 'primary');
		if (sizeof($pk_columns) == 1 && $schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) {
			$this->values[$pk_columns[0]] = NULL;
			unset($this->old_values[$pk_columns[0]]);
		}
		
		return $this;
	}
	
	
	/**
	 * Retrieves a value from the record and prepares it for output into an HTML form element.
	 * 
	 * Below are the transformations performed:
	 *  
	 *  - **varchar, char, text**: will run through fHTML::encode(), if `TRUE` is passed the text will be run through fHTML::convertNewLinks() and fHTML::makeLinks()
	 *  - **float**: takes 1 parameter to specify the number of decimal places
	 *  - **date, time, timestamp**: `format()` will be called on the fDate/fTime/fTimestamp object with the 1 parameter specified
	 *  - **objects**: the object will be converted to a string by `__toString()` or a `(string)` cast and then will be run through fHTML::encode()
	 *  - **all other data types**: the value will be run through fHTML::encode()
	 * 
	 * @param  string $column      The name of the column to retrieve
	 * @param  string $formatting  The formatting string
	 * @return string  The encoded value for the column specified
	 */
	protected function encode($column, $formatting=NULL)
	{
		$column_exists = array_key_exists($column, $this->values);
		$method_name   = 'get' . fGrammar::camelize($column, TRUE);
		$method_exists = method_exists($this, $method_name);
		
		if (!$column_exists && !$method_exists) {
			throw new fProgrammerException(
				'The column specified, %s, does not exist',
				$column
			);
		}
		
		if ($column_exists) {
			$class       = get_class($this);
			$schema      = fORMSchema::retrieve($class);
			$table       = fORM::tablize($class);
			$column_type = $schema->getColumnInfo($table, $column, 'type');
			
			// Ensure the programmer is calling the function properly
			if ($column_type == 'blob') {
				throw new fProgrammerException(
					'The column specified, %s, does not support forming because it is a blob column',
					$column
				);
			}
			
			if ($formatting !== NULL && in_array($column_type, array('boolean', 'integer'))) {
				throw new fProgrammerException(
					'The column specified, %s, does not support any formatting options',
					$column
				);
			}
			
		// If the column doesn't exist, we are just pulling the
		// value from a get method, so treat it as text
		} else {
			$column_type = 'text';	
		}
		
		// Grab the value for empty value checking
		$value = $this->$method_name();
		
		// Date/time objects
		if (is_object($value) && in_array($column_type, array('date', 'time', 'timestamp'))) {
			if ($formatting === NULL) {
				throw new fProgrammerException(
					'The column specified, %s, requires one formatting parameter, a valid date() formatting string',
					$column
				);
			}
			$value = $value->format($formatting);
		}
		
		// Other objects
		if (is_object($value) && is_callable(array($value, '__toString'))) {
			$value = $value->__toString();
		} elseif (is_object($value)) {
			$value = (string) $value;	
		}
		
		// Make sure we don't mangle a non-float value
		if ($column_type == 'float' && is_numeric($value)) {
			$column_decimal_places = $schema->getColumnInfo($table, $column, 'decimal_places');
			
			// If the user passed in a formatting value, use it
			if ($formatting !== NULL && is_numeric($formatting)) {
				$decimal_places = (int) $formatting;
				
			// If the column has a pre-defined number of decimal places, use that
			} elseif ($column_decimal_places !== NULL) {
				$decimal_places = $column_decimal_places;
			
			// This figures out how many decimal places are part of the current value
			} else {
				$value_parts    = explode('.', $value);
				$decimal_places = (!isset($value_parts[1])) ? 0 : strlen($value_parts[1]);
			}
			
			return number_format($value, $decimal_places, '.', '');
		}
		
		// Turn line-breaks into breaks for text fields and add links
		if ($formatting === TRUE && in_array($column_type, array('varchar', 'char', 'text'))) {
			return fHTML::makeLinks(fHTML::convertNewlines(fHTML::encode($value)));
		}
		
		// Anything that has gotten to here is a string value or is not the proper data type for the column that contains it
		return fHTML::encode($value);
	}
	
	
	/**
	 * Checks to see if the record exists in the database
	 * 
	 * @return boolean  If the record exists in the database
	 */
	public function exists()
	{
		$class = get_class($this);
		
		if (fORM::getActiveRecordMethod($class, 'exists')) {
			return $this->__call('exists', array());
		}
		
		$schema     = fORMSchema::retrieve($class);
		$table      = fORM::tablize($class);
		$pk_columns = $schema->getKeys($table, 'primary');
		$exists     = FALSE;
		
		foreach ($pk_columns as $pk_column) {
			$has_old = self::hasOld($this->old_values, $pk_column);
			if (($has_old && self::retrieveOld($this->old_values, $pk_column) !== NULL) || (!$has_old && $this->values[$pk_column] !== NULL)) {
				$exists = TRUE;
			}
		}
		
		return $exists;
	}
	
	
	/**
	 * Loads a record from the database based on a UNIQUE key
	 * 
	 * @throws fNotFoundException
	 * 
	 * @param  array $values  The UNIQUE key values to try and load with
	 * @return void
	 */
	protected function fetchResultFromUniqueKey($values)
	{		
		$class = get_class($this);
		
		$db     = fORMDatabase::retrieve($class, 'read');
		$schema = fORMSchema::retrieve($class);
		
		try {
			if ($values === array_combine(array_keys($values), array_fill(0, sizeof($values), NULL))) {
				throw new fExpectedException('The values specified for the unique key are all NULL');	
			}
			
			$table  = fORM::tablize($class);
			$params = array('SELECT * FROM %r WHERE ', $table);
			
			$column_info = $schema->getColumnInfo($table);
			
			$conditions = array();
			foreach ($values as $column => $value) {
				
				// This makes sure the query performs the way an insert will
				if ($value === NULL && $column_info[$column]['not_null'] && $column_info[$column]['default'] !== NULL) {
					$value = $column_info[$column]['default'];
				}
				
				$conditions[] = fORMDatabase::makeCondition($schema, $table, $column, '=', $value);
				$params[] = $column;
				$params[] = $value;	
			}
			
			$params[0] .= join(' AND ', $conditions);
		
			$result = call_user_func_array($db->translatedQuery, $params);
			$result->tossIfNoRows();
			
		} catch (fExpectedException $e) {
			throw new fNotFoundException(
				'The %s requested could not be found',
				fORM::getRecordName($class)
			);
		}
		
		return $result;
	}
	
	
	/**
	 * Retrieves a value from the record
	 * 
	 * @param  string $column  The name of the column to retrieve
	 * @return mixed  The value for the column specified
	 */
	protected function get($column)
	{
		if (!isset($this->values[$column]) && !array_key_exists($column, $this->values)) {
			throw new fProgrammerException(
				'The column specified, %s, does not exist',
				$column
			);
		}
		return $this->values[$column];
	}
	
	
	/**
	 * Retrieves information about a column
	 * 
	 * @param  string $column   The name of the column to inspect
	 * @param  string $element  The metadata element to retrieve
	 * @return mixed  The metadata array for the column, or the metadata element specified
	 */
	protected function inspect($column, $element=NULL)
	{
		if (!array_key_exists($column, $this->values)) {
			throw new fProgrammerException(
				'The column specified, %s, does not exist',
				$column
			);
		}
		
		$class  = get_class($this);
		$table  = fORM::tablize($class);
		$schema = fORMSchema::retrieve($class);
		$info   = $schema->getColumnInfo($table, $column);
		
		if (!in_array($info['type'], array('varchar', 'char', 'text'))) {
			unset($info['valid_values']);
			unset($info['max_length']);
		}
		
		if ($info['type'] != 'float') {
			unset($info['decimal_places']);
		}
		
		if ($info['type'] != 'integer') {
			unset($info['auto_increment']);
		}
		
		if (!in_array($info['type'], array('integer', 'float'))) {
			unset($info['min_value']);
			unset($info['max_value']);
		}
		
		$info['feature'] = NULL;
		
		fORM::callInspectCallbacks(get_class($this), $column, $info);
		
		if ($element) {
			if (!isset($info[$element]) && !array_key_exists($element, $info)) {
				throw new fProgrammerException(
					'The element specified, %1$s, is invalid. Must be one of: %2$s.',
					$element,
					join(', ', array_keys($info))
				);
			}
			return $info[$element];
		}
		
		return $info;
	}
	
	
	/**
	 * Loads a record from the database
	 * 
	 * @throws fNotFoundException  When the record could not be found in the database
	 * 
	 * @return fActiveRecord  The record object, to allow for method chaining
	 */
	public function load()
	{
		$class  = get_class($this);
		$db     = fORMDatabase::retrieve($class, 'read');
		$schema = fORMSchema::retrieve($class);
		
		if (fORM::getActiveRecordMethod($class, 'load')) {
			return $this->__call('load', array());
		}
		
		try {
			$table = fORM::tablize($class);
			$params = array('SELECT * FROM %r WHERE ', $table);
			$params = fORMDatabase::addPrimaryKeyWhereParams($schema, $params, $table, $table, $this->values, $this->old_values);
		
			$result = call_user_func_array($db->translatedQuery, $params);
			$result->tossIfNoRows();
			
		} catch (fExpectedException $e) {
			throw new fNotFoundException(
				'The %s requested could not be found',
				fORM::getRecordName($class)
			);
		}
		
		$this->loadFromResult($result, TRUE);
		
		// Clears the cached related records so they get pulled from the database
		$this->related_records = array();
		
		return $this;
	}
	
	
	/**
	 * Loads a record from the database directly from a result object
	 * 
	 * @param  Iterator $result               The result object to use for loading the current object
	 * @param  boolean  $ignore_identity_map  If the identity map should be ignored and the values loaded no matter what
	 * @return boolean  If the record was loaded from the identity map
	 */
	protected function loadFromResult($result, $ignore_identity_map=FALSE)
	{
		$class  = get_class($this);
		$table  = fORM::tablize($class);
		$row    = $result->current();
		
		$db     = fORMDatabase::retrieve($class, 'read');
		$schema = fORMSchema::retrieve($class);
		
		if (!isset(self::$unescape_map[$class])) {
			self::$unescape_map[$class] = array();
			$column_info                = $schema->getColumnInfo($table);
			
			foreach ($column_info as $column => $info) {
				if (in_array($info['type'], array('blob', 'boolean', 'date', 'time', 'timestamp'))) {
					self::$unescape_map[$class][$column] = $info['type'];
				}
			}	
		}
		
		$pk_columns = $schema->getKeys($table, 'primary');
		foreach ($pk_columns as $column) {
			$value = $row[$column];
			if ($value !== NULL && isset(self::$unescape_map[$class][$column])) {
				$value = $db->unescape(self::$unescape_map[$class][$column], $value);
			}	
			
			$this->values[$column] = fORM::objectify($class, $column, $value);
			unset($row[$column]);
		}
		
		$hash = self::hash($this->values, $class);
		if (!$ignore_identity_map && $this->loadFromIdentityMap($this->values, $hash)) {
			return TRUE;
		}
		
		foreach ($row as $column => $value) {
			if ($value !== NULL && isset(self::$unescape_map[$class][$column])) {
				$value = $db->unescape(self::$unescape_map[$class][$column], $value);
			}
			
			$this->values[$column] = fORM::objectify($class, $column, $value);
		}
		
		// Save this object to the identity map
		if (!isset(self::$identity_map[$class])) {
			self::$identity_map[$class] = array(); 		
		}
		self::$identity_map[$class][$hash] = $this;
		
		fORM::callHookCallbacks(
			$this,
			'post::loadFromResult()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		return FALSE;
	}
	
	
	/**
	 * Tries to load the object (via references to class vars) from the fORM identity map
	 * 
	 * @param  array  $row   The data source for the primary key values
	 * @param  string $hash  The unique hash for this record
	 * @return boolean  If the load was successful
	 */
	protected function loadFromIdentityMap($row, $hash)
	{
		$class = get_class($this);
		
		if (!isset(self::$identity_map[$class])) {
			return FALSE;
		}
		
		if (!isset(self::$identity_map[$class][$hash])) {
			return FALSE;
		}
		
		$object = self::$identity_map[$class][$hash];
		
		// If we got a result back, it is the object we are creating
		$this->cache           = &$object->cache;
		$this->values          = &$object->values;
		$this->old_values      = &$object->old_values;
		$this->related_records = &$object->related_records;
		
		fORM::callHookCallbacks(
			$this,
			'post::loadFromIdentityMap()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		return TRUE;
	}
	
	
	/**
	 * Sets the values for this record by getting values from the request through the fRequest class
	 * 
	 * @return fActiveRecord  The record object, to allow for method chaining
	 */
	public function populate()
	{
		$class = get_class($this);
		
		if (fORM::getActiveRecordMethod($class, 'populate')) {
			return $this->__call('populate', array());
		}
		
		fORM::callHookCallbacks(
			$this,
			'pre::populate()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		$schema = fORMSchema::retrieve($class);
		$table  = fORM::tablize($class);
		
		$column_info = $schema->getColumnInfo($table);
		foreach ($column_info as $column => $info) {
			if (fRequest::check($column)) {
				$method = 'set' . fGrammar::camelize($column, TRUE);
				$cast_to = ($info['type'] == 'blob') ? 'binary' : NULL;
				$this->$method(fRequest::get($column, $cast_to));
			}
		}
		
		fORM::callHookCallbacks(
			$this,
			'post::populate()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		return $this;
	}
	
	
	/**
	 * Retrieves a value from the record and prepares it for output into html.
	 * 
	 * Below are the transformations performed:
	 * 
	 *  - **varchar, char, text**: will run through fHTML::prepare(), if `TRUE` is passed the text will be run through fHTML::convertNewLinks() and fHTML::makeLinks()
	 *  - **boolean**: will return `'Yes'` or `'No'`
	 *  - **integer**: will add thousands/millions/etc. separators
	 *  - **float**: will add thousands/millions/etc. separators and takes 1 parameter to specify the number of decimal places
	 *  - **date, time, timestamp**: `format()` will be called on the fDate/fTime/fTimestamp object with the 1 parameter specified
	 *  - **objects**: the object will be converted to a string by `__toString()` or a `(string)` cast and then will be run through fHTML::prepare()
	 * 
	 * @param  string $column      The name of the column to retrieve
	 * @param  mixed  $formatting  The formatting parameter, if applicable
	 * @return string  The formatted value for the column specified
	 */
	protected function prepare($column, $formatting=NULL)
	{
		$column_exists = array_key_exists($column, $this->values);
		$method_name   = 'get' . fGrammar::camelize($column, TRUE);
		$method_exists = method_exists($this, $method_name);
		
		if (!$column_exists && !$method_exists) {
			throw new fProgrammerException(
				'The column specified, %s, does not exist',
				$column
			);
		}
		
		if ($column_exists) {
			$class  = get_class($this);
			$table  = fORM::tablize($class);
			$schema = fORMSchema::retrieve($class);
			
			$column_info = $schema->getColumnInfo($table, $column);
			$column_type = $column_info['type'];
			
			// Ensure the programmer is calling the function properly
			if ($column_type == 'blob') {
				throw new fProgrammerException(
					'The column specified, %s, can not be prepared because it is a blob column',
					$column
				);
			}
			
			if ($formatting !== NULL && in_array($column_type, array('integer', 'boolean'))) {
				throw new fProgrammerException(
					'The column specified, %s, does not support any formatting options',
					$column
				);
			}
		
		// If the column doesn't exist, we are just pulling the
		// value from a get method, so treat it as text
		} else {
			$column_type = 'text';	
		}
		
		// Grab the value for empty value checking
		$value = $this->$method_name();
		
		// Date/time objects
		if (is_object($value) && in_array($column_type, array('date', 'time', 'timestamp'))) {
			if ($formatting === NULL) {
				throw new fProgrammerException(
					'The column specified, %s, requires one formatting parameter, a valid date() formatting string',
					$column
				);
			}
			return $value->format($formatting);
		}
		
		// Other objects
		if (is_object($value) && is_callable(array($value, '__toString'))) {
			$value = $value->__toString();
		} elseif (is_object($value)) {
			$value = (string) $value;	
		}
		
		// Ensure the value matches the data type specified to prevent mangling
		if ($column_type == 'boolean' && is_bool($value)) {
			return ($value) ? 'Yes' : 'No';
		}
		
		if ($column_type == 'integer' && is_numeric($value)) {
			return number_format($value, 0, '', ',');
		}
		
		if ($column_type == 'float' && is_numeric($value)) {
			// If the user passed in a formatting value, use it
			if ($formatting !== NULL && is_numeric($formatting)) {
				$decimal_places = (int) $formatting;
				
			// If the column has a pre-defined number of decimal places, use that
			} elseif ($column_info['decimal_places'] !== NULL) {
				$decimal_places = $column_info['decimal_places'];
			
			// This figures out how many decimal places are part of the current value
			} else {
				$value_parts    = explode('.', $value);
				$decimal_places = (!isset($value_parts[1])) ? 0 : strlen($value_parts[1]);
			}
			
			return number_format($value, $decimal_places, '.', ',');
		}
		
		// Turn line-breaks into breaks for text fields and add links
		if ($formatting === TRUE && in_array($column_type, array('varchar', 'char', 'text'))) {
			return fHTML::makeLinks(fHTML::convertNewlines(fHTML::prepare($value)));
		}
		
		// Anything that has gotten to here is a string value, or is not the
		// proper data type for the column, so we just make sure it is marked
		// up properly for display in HTML
		return fHTML::prepare($value);
	}
	
	
	/**
	 * Generates the method signatures for all methods (including dynamic ones)
	 * 
	 * @param  boolean $include_doc_comments  If the doc block comments for each method should be included
	 * @return array  An associative array of method name => method signature
	 */
	public function reflect($include_doc_comments=FALSE)
	{
		$signatures = array();
		
		$class        = get_class($this);
		$table        = fORM::tablize($class);
		$schema       = fORMSchema::retrieve($class);
		$columns_info = $schema->getColumnInfo($table);
		foreach ($columns_info as $column => $column_info) {
			$camelized_column = fGrammar::camelize($column, TRUE);
			
			// Get and set methods
			$signature = '';
			if ($include_doc_comments) {
				$fixed_type = $column_info['type'];
				if ($fixed_type == 'blob') {
					$fixed_type = 'string';
				}
				if ($fixed_type == 'date') {
					$fixed_type = 'fDate';
				}
				if ($fixed_type == 'timestamp') {
					$fixed_type = 'fTimestamp';
				}
				if ($fixed_type == 'time') {
					$fixed_type = 'fTime';
				}
				
				$signature .= "/**\n";
				$signature .= " * Gets the current value of " . $column . "\n";
				$signature .= " * \n";
				$signature .= " * @return " . $fixed_type . "  The current value\n";
				$signature .= " */\n";
			}
			$get_method = 'get' . $camelized_column;
			$signature .= 'public function ' . $get_method . '()';
			
			$signatures[$get_method] = $signature;
			
			
			$signature = '';
			if ($include_doc_comments) {
				$fixed_type = $column_info['type'];
				if ($fixed_type == 'blob') {
					$fixed_type = 'string';
				}
				if ($fixed_type == 'date') {
					$fixed_type = 'fDate|string';
				}
				if ($fixed_type == 'timestamp') {
					$fixed_type = 'fTimestamp|string';
				}
				if ($fixed_type == 'time') {
					$fixed_type = 'fTime|string';
				}
				
				$signature .= "/**\n";
				$signature .= " * Sets the value for " . $column . "\n";
				$signature .= " * \n";
				$signature .= " * @param  " . $fixed_type . " \$" . $column . "  The new value\n";
				$signature .= " * @return fActiveRecord  The record object, to allow for method chaining\n";
				$signature .= " */\n";
			}
			$set_method = 'set' . $camelized_column;
			$signature .= 'public function ' . $set_method . '($' . $column . ')';
			
			$signatures[$set_method] = $signature;
			
			
			// The encode method
			$signature = '';
			if ($include_doc_comments) {
				$signature .= "/**\n";
				$signature .= " * Encodes the value of " . $column . " for output into an HTML form\n";
				$signature .= " * \n";
				
				if (in_array($column_info['type'], array('time', 'timestamp', 'date'))) {
					$signature .= " * @param  string \$date_formatting_string  A date() compatible formatting string\n";
				}
				if (in_array($column_info['type'], array('float'))) {
					$signature .= " * @param  integer \$decimal_places  The number of decimal places to include - if not specified will default to the precision of the column or the current value\n";
				}
				
				$signature .= " * @return string  The HTML form-ready value\n";
				$signature .= " */\n";
			}
			$encode_method = 'encode' . $camelized_column;
			$signature .= 'public function ' . $encode_method . '(';
			if (in_array($column_info['type'], array('time', 'timestamp', 'date'))) {
				$signature .= '$date_formatting_string';
			}
			if (in_array($column_info['type'], array('float'))) {
				$signature .= '$decimal_places=NULL';
			}
			$signature .= ')';
			
			$signatures[$encode_method] = $signature;
			
			
			// The prepare method
			$signature = '';
			if ($include_doc_comments) {
				$signature .= "/**\n";
				$signature .= " * Prepares the value of " . $column . " for output into HTML\n";
				$signature .= " * \n";
				
				if (in_array($column_info['type'], array('time', 'timestamp', 'date'))) {
					$signature .= " * @param  string \$date_formatting_string  A date() compatible formatting string\n";
				}
				if (in_array($column_info['type'], array('float'))) {
					$signature .= " * @param  integer \$decimal_places  The number of decimal places to include - if not specified will default to the precision of the column or the current value\n";
				}
				if (in_array($column_info['type'], array('varchar', 'char', 'text'))) {
					$signature .= " * @param  boolean \$create_links_and_line_breaks  Will cause links to be automatically converted into [a] tags and line breaks into [br] tags \n";
				}
				
				$signature .= " * @return string  The HTML-ready value\n";
				$signature .= " */\n";
			}
			$prepare_method = 'prepare' . $camelized_column;
			$signature .= 'public function ' . $prepare_method . '(';
			if (in_array($column_info['type'], array('time', 'timestamp', 'date'))) {
				$signature .= '$date_formatting_string';
			}
			if (in_array($column_info['type'], array('float'))) {
				$signature .= '$decimal_places=NULL';
			}
			if (in_array($column_info['type'], array('varchar', 'char', 'text'))) {
				$signature .= '$create_links_and_line_breaks=FALSE';
			}
			$signature .= ')';
			
			$signatures[$prepare_method] = $signature;
			
			
			// The inspect method
			$signature = '';
			if ($include_doc_comments) {
				$signature .= "/**\n";
				$signature .= " * Returns metadata about " . $column . "\n";
				$signature .= " * \n";
				$elements = array('type', 'not_null', 'default', 'comment');
				if (in_array($column_info['type'], array('varchar', 'char', 'text'))) {
					$elements[] = 'valid_values';
					$elements[] = 'max_length';
				}
				if ($column_info['type'] == 'float') {
					$elements[] = 'decimal_places';
				}
				if ($column_info['type'] == 'integer') {
					$elements[] = 'auto_increment';
					$elements[] = 'min_value';
					$elements[] = 'max_value';
				}
				$signature .= " * @param  string \$element  The element to return. Must be one of: '" . join("', '", $elements) . "'.\n";
				$signature .= " * @return mixed  The metadata array or a single element\n";
				$signature .= " */\n";
			}
			$inspect_method = 'inspect' . $camelized_column;
			$signature .= 'public function ' . $inspect_method . '($element=NULL)';
			
			$signatures[$inspect_method] = $signature;
		}
		
		fORMRelated::reflect($class, $signatures, $include_doc_comments);
		
		fORM::callReflectCallbacks($class, $signatures, $include_doc_comments);
		
		$reflection = new ReflectionClass($class);
		$methods    = $reflection->getMethods();
		
		foreach ($methods as $method) {
			$signature = '';
			
			if (!$method->isPublic() || $method->getName() == '__call') {
				continue;
			}
			
			if ($method->isFinal()) {
				$signature .= 'final ';
			}
			
			if ($method->isAbstract()) {
				$signature .= 'abstract ';
			}
			
			if ($method->isStatic()) {
				$signature .= 'static ';
			}
			
			$signature .= 'public function ';
			
			if ($method->returnsReference()) {
				$signature .= '&';
			}
			
			$signature .= $method->getName();
			$signature .= '(';
			
			$parameters = $method->getParameters();
			foreach ($parameters as $parameter) {
				if (substr($signature, -1) == '(') {
					$signature .= '';
				} else {
					$signature .= ', ';
				}
				
				if ($parameter->isArray()) {
					$signature .= 'array ';	
				}
				if ($parameter->getClass()) {
					$signature .= $parameter->getClass()->getName() . ' ';	
				}
				if ($parameter->isPassedByReference()) {
					$signature .= '&';	
				}
				$signature .= '$' . $parameter->getName();
				
				if ($parameter->isDefaultValueAvailable()) {
					$val = var_export($parameter->getDefaultValue(), TRUE);
					if ($val == 'true') {
						$val = 'TRUE';
					}
					if ($val == 'false') {
						$val = 'FALSE';
					}
					if (is_array($parameter->getDefaultValue())) {
						$val = preg_replace('#array\s+\(\s+#', 'array(', $val);
						$val = preg_replace('#,(\r)?\n  #', ', ', $val);
						$val = preg_replace('#,(\r)?\n\)#', ')', $val);
					}
					$signature .= '=' . $val;
				}
			}
			
			$signature .= ')';
			
			if ($include_doc_comments) {
				$comment = $method->getDocComment();
				$comment = preg_replace('#^\t+#m', '', $comment);
				$signature = $comment . "\n" . $signature;
			}
			$signatures[$method->getName()] = $signature;
		}
		
		ksort($signatures);
		
		return $signatures;
	}
	
	
	/**
	 * Generates a clone of the current record, removing any auto incremented primary key value and allowing for replicating related records
	 * 
	 * This method will accept three different sets of parameters:
	 * 
	 *  - No parameters: this object will be cloned
	 *  - A single `TRUE` value: this object plus all many-to-many associations and all child records (recursively) will be cloned
	 *  - Any number of plural related record class names: the many-to-many associations or child records that correspond to the classes specified will be cloned
	 * 
	 * The class names specified can be a simple class name if there is only a
	 * single route between the two corresponding database tables. If there is 
	 * more than one route between the two tables, the class name should be
	 * substituted with a string in the format `'RelatedClass{route}'`.
	 * 
	 * @param  string $related_class  The plural related class to replicate - see method description for details
	 * @param  string ...
	 * @return fActiveRecord  The cloned record
	 */
	public function replicate($related_class=NULL)
	{
		fORM::callHookCallbacks(
			$this,
			'pre::replicate()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache,
			fActiveRecord::$replicate_level
		);
		
		fActiveRecord::$replicate_level++;
		
		$class  = get_class($this);
		$hash   = self::hash($this->values, $class);
		$schema = fORMSchema::retrieve($class);
		$table  = fORM::tablize($class);
			
		// If the object has not been replicated yet, do it now
		if (!isset(fActiveRecord::$replicate_map[$class])) {
			fActiveRecord::$replicate_map[$class] = array();
		}
		if (!isset(fActiveRecord::$replicate_map[$class][$hash])) {
			fActiveRecord::$replicate_map[$class][$hash] = clone $this;
			
			// We need the primary key to get a hash, otherwise certain recursive relationships end up losing members
			$pk_columns = $schema->getKeys($table, 'primary');
			if (sizeof($pk_columns) == 1 && $schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) {
				fActiveRecord::$replicate_map[$class][$hash]->values[$pk_columns[0]] = $this->values[$pk_columns[0]];
			}
			
		}
		$clone = fActiveRecord::$replicate_map[$class][$hash];
		
		$parameters = func_get_args();
		
		$recursive                  = FALSE;
		$many_to_many_relationships = $schema->getRelationships($table, 'many-to-many');
		$one_to_many_relationships  = $schema->getRelationships($table, 'one-to-many');
		
		
		// When just TRUE is passed we recursively replicate all related records
		if (sizeof($parameters) == 1 && $parameters[0] === TRUE) {
			$parameters = array();
			$recursive  = TRUE;
			
			foreach ($many_to_many_relationships as $relationship) {
				$parameters[] = fGrammar::pluralize(fORM::classize($relationship['related_table'])) . '{' . $relationship['join_table'] . '}'; 		
			}
			foreach ($one_to_many_relationships as $relationship) {
				$parameters[] = fGrammar::pluralize(fORM::classize($relationship['related_table'])) . '{' . $relationship['related_column'] . '}'; 		
			}			
		}
		
		$record_sets = array();
		
		foreach ($parameters as $parameter) {
			
			// Parse the Class{route} strings
			if (strpos($parameter, '{') !== FALSE) {
				$brace         = strpos($parameter, '{');
				$related_class = fGrammar::singularize(substr($parameter, 0, $brace));
				$related_class = fORM::getRelatedClass($class, $related_class);
				$related_table = fORM::tablize($related_class);
				$route         = substr($parameter, $brace+1, -1);
			} else {
				$related_class = fGrammar::singularize($parameter);
				$related_class = fORM::getRelatedClass($class, $related_class);
				$related_table = fORM::tablize($related_class);
				$route         = fORMSchema::getRouteName($schema, $table, $related_table);
			}
			
			// Determine the kind of relationship
			$many_to_many = FALSE;
			$one_to_many  = FALSE;
			
			foreach ($many_to_many_relationships as $relationship) {
				if ($relationship['related_table'] == $related_table && $relationship['join_table'] == $route) {
					$many_to_many = TRUE;	
					break;
				}
			}
			
			foreach ($one_to_many_relationships as $relationship) {
				if ($relationship['related_table'] == $related_table && $relationship['related_column'] == $route) {
					$one_to_many = TRUE;
					break;
				}	
			}
			
			if (!$many_to_many && !$one_to_many) {
				throw new fProgrammerException(
					'The related class specified, %1$s, does not appear to be in a many-to-many or one-to-many relationship with %$2s',
					$parameter,
					get_class($this)
				);	
			}
			
			// Get the related records
			$record_set = fORMRelated::buildRecords($class, $this->values, $this->related_records, $related_class, $route);
			
			// One-to-many records need to be replicated, possibly recursively
			if ($one_to_many) {
				if ($recursive) {
					$records = $record_set->call('replicate', TRUE);
				} else {
					$records = $record_set->call('replicate');
				}
				$record_set = fRecordSet::buildFromArray($related_class, $records);
				$record_set->call(
					'set' . fGrammar::camelize($route, TRUE),
					NULL
				);	
			}
			
			// Cause the related records to be associated with the new clone
			fORMRelated::associateRecords($class, $clone->related_records, $related_class, $record_set, $route);
		}
		
		fActiveRecord::$replicate_level--;
		if (!fActiveRecord::$replicate_level) {
			// This removes the primary keys we had added back in for proper duplicate detection
			foreach (fActiveRecord::$replicate_map as $class => $records) {
				$table      = fORM::tablize($class);
				$pk_columns = $schema->getKeys($table, 'primary');
				if (sizeof($pk_columns) != 1 || !$schema->getColumnInfo($table, $pk_columns[0], 'auto_increment')) {
					continue;
				}
				foreach ($records as $hash => $record) {
					$record->values[$pk_columns[0]] = NULL;		
				}	
			}
			fActiveRecord::$replicate_map = array();	
		}
		
		fORM::callHookCallbacks(
			$this,
			'post::replicate()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache,
			fActiveRecord::$replicate_level
		);
		
		fORM::callHookCallbacks(
			$clone,
			'cloned::replicate()',
			$clone->values,
			$clone->old_values,
			$clone->related_records,
			$clone->cache,
			fActiveRecord::$replicate_level
		);
		
		return $clone;
	}
	
	
	/**
	 * Sets a value to the record
	 * 
	 * @param  string $column  The column to set the value to
	 * @param  mixed  $value   The value to set
	 * @return fActiveRecord  This record, to allow for method chaining
	 */
	protected function set($column, $value)
	{
		if (!array_key_exists($column, $this->values)) {
			throw new fProgrammerException(
				'The column specified, %s, does not exist',
				$column
			);
		}
		
		// We consider an empty string or a string of spaces to be equivalent to NULL
		if ($value === '' || (is_string($value) && trim($value) === '')) {
			$value = NULL;
		}
		
		$class = get_class($this);
		$value = fORM::objectify($class, $column, $value);
		
		// Float and int columns that look like numbers with commas will have the commas removed
		if (is_string($value)) {
			$table  = fORM::tablize($class);
			$schema = fORMSchema::retrieve($class);
			$type   = $schema->getColumnInfo($table, $column, 'type');
			if (in_array($type, array('integer', 'float')) && preg_match('#^(\d+,)+\d+(\.\d+)?$#', $value)) {
				$value = str_replace(',', '', $value);
			}
		}
		
		self::assign($this->values, $this->old_values, $column, $value);
		
		return $this;
	}
	
	
	/**
	 * Stores a record in the database, whether existing or new
	 * 
	 * This method will start database and filesystem transactions if they have
	 * not already been started.
	 * 
	 * @throws fValidationException  When ::validate() throws an exception
	 * 
	 * @param  boolean $force_cascade  When storing related records, this will force deleting child records even if they have their own children in a relationship with an RESTRICT or NO ACTION for the ON DELETE clause
	 * @return fActiveRecord  The record object, to allow for method chaining
	 */
	public function store($force_cascade=FALSE)
	{
		$class = get_class($this);
		
		if (fORM::getActiveRecordMethod($class, 'store')) {
			return $this->__call('store', array());
		}
		
		fORM::callHookCallbacks(
			$this,
			'pre::store()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		$db     = fORMDatabase::retrieve($class, 'write');
		$schema = fORMSchema::retrieve($class);
		
		try {
			$table = fORM::tablize($class);
			
			// New auto-incrementing records require lots of special stuff, so we'll detect them here
			$new_autoincrementing_record = FALSE;
			if (!$this->exists()) {
				$pk_columns           = $schema->getKeys($table, 'primary');
				$pk_column            = $pk_columns[0];
				$pk_auto_incrementing = $schema->getColumnInfo($table, $pk_column, 'auto_increment');
				
				if (sizeof($pk_columns) == 1 && $pk_auto_incrementing && !$this->values[$pk_column]) {
					$new_autoincrementing_record = TRUE;
				}
			}
			
			$inside_db_transaction = $db->isInsideTransaction();
			
			if (!$inside_db_transaction) {
				$db->translatedQuery('BEGIN');
			}
			
			fORM::callHookCallbacks(
				$this,
				'post-begin::store()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			$this->validate();
			
			fORM::callHookCallbacks(
				$this,
				'post-validate::store()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			// Storing main table
			
			if (!$this->exists()) {
				$params = $this->constructInsertParams();
			} else {
				$params = $this->constructUpdateParams();
			}
			$result = call_user_func_array($db->translatedQuery, $params);
			
			
			// If there is an auto-incrementing primary key, grab the value from the database
			if ($new_autoincrementing_record) {
				$this->set($pk_column, $result->getAutoIncrementedValue());
			}
			
			
			// Fix cascade updated columns for in-memory objects to prevent issues when saving
			$one_to_one_relationships  = $schema->getRelationships($table, 'one-to-one');
			$one_to_many_relationships = $schema->getRelationships($table, 'one-to-many');
			
			$relationships = array_merge($one_to_one_relationships, $one_to_many_relationships);
			
			foreach ($relationships as $relationship) {
				$type  = in_array($relationship, $one_to_one_relationships) ? 'one-to-one' : 'one-to-many';
				$route = fORMSchema::getRouteNameFromRelationship($type, $relationship);
				
				$related_table = $relationship['related_table'];
				$related_class = fORM::classize($related_table);
				$related_class = fORM::getRelatedClass($class, $related_class);
				
				if ($relationship['on_update'] != 'cascade') {
					continue;
				}
				
				$column = $relationship['column'];
				if (!fActiveRecord::changed($this->values, $this->old_values, $column)) {
					continue;
				}
				
				if (!isset($this->related_records[$related_table][$route]['record_set'])) {
					continue;
				}
				
				$record_set     = $this->related_records[$related_table][$route]['record_set'];
				$related_column = $relationship['related_column'];
				
				$old_value      = fActiveRecord::retrieveOld($this->old_values, $column);
				$value          = $this->values[$column];
				
				if ($old_value === NULL) {
					continue;
				}
				foreach ($record_set as $record) {
					if (isset($record->old_values[$related_column])) {
						foreach (array_keys($record->old_values[$related_column]) as $key) {
							if ($record->old_values[$related_column][$key] === $old_value) {
								$record->old_values[$related_column][$key] = $value;
							}
						}
					}
					if ($record->values[$related_column] === $old_value) {
						$record->values[$related_column] = $value;
					}
				}
			}
			
			// Storing *-to-many and one-to-one relationships
			fORMRelated::store($class, $this->values, $this->related_records, $force_cascade);
			
			
			fORM::callHookCallbacks(
				$this,
				'pre-commit::store()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			if (!$inside_db_transaction) {
				$db->translatedQuery('COMMIT');
			}
			
			fORM::callHookCallbacks(
				$this,
				'post-commit::store()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
		} catch (fException $e) {
			
			if (!$inside_db_transaction) {
				$db->translatedQuery('ROLLBACK');
			}
			
			fORM::callHookCallbacks(
				$this,
				'post-rollback::store()',
				$this->values,
				$this->old_values,
				$this->related_records,
				$this->cache
			);
			
			if ($new_autoincrementing_record && self::hasOld($this->old_values, $pk_column)) {
				$this->values[$pk_column] = self::retrieveOld($this->old_values, $pk_column);
				unset($this->old_values[$pk_column]);
			}
			
			throw $e;
		}
		
		fORM::callHookCallbacks(
			$this,
			'post::store()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache
		);
		
		$was_new = !$this->exists();
		
		// If we got here we succefully stored, so update old values to make exists() work
		foreach ($this->values as $column => $value) {
			$this->old_values[$column] = array($value);
		}
		
		// If the object was just inserted into the database, save it to the identity map
		if ($was_new) {
			$hash = self::hash($this->values, $class);
			
			if (!isset(self::$identity_map[$class])) {
				self::$identity_map[$class] = array(); 		
			}
			self::$identity_map[$class][$hash] = $this;		
		}
		
		return $this;
	}
	
	
	/**
	 * Validates the values of the record against the database and any additional validation rules
	 * 
	 * @throws fValidationException  When the record, or one of the associated records, violates one of the validation rules for the class or can not be properly stored in the database
	 * 
	 * @param  boolean $return_messages      If an array of validation messages should be returned instead of an exception being thrown
	 * @param  boolean $remove_column_names  If column names should be removed from the returned messages, leaving just the message itself
	 * @return void|array  If $return_messages is TRUE, an array of validation messages will be returned
	 */
	public function validate($return_messages=FALSE, $remove_column_names=FALSE)
	{
		$class = get_class($this);
		
		if (fORM::getActiveRecordMethod($class, 'validate')) {
			return $this->__call('validate', array($return_messages));
		}
		
		$validation_messages = array();
		
		fORM::callHookCallbacks(
			$this,
			'pre::validate()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache,
			$validation_messages
		);
		
		// Validate the local values
		$local_validation_messages = fORMValidation::validate($this, $this->values, $this->old_values);
		
		// Validate related records
		$related_validation_messages = fORMValidation::validateRelated($this, $this->values, $this->related_records);
		
		$validation_messages = array_merge($validation_messages, $local_validation_messages, $related_validation_messages);
		
		fORM::callHookCallbacks(
			$this,
			'post::validate()',
			$this->values,
			$this->old_values,
			$this->related_records,
			$this->cache,
			$validation_messages
		);
		
		$validation_messages = fORMValidation::replaceMessages($class, $validation_messages);
		$validation_messages = fORMValidation::reorderMessages($class, $validation_messages);
		
		if ($return_messages) {
			if ($remove_column_names) {
				$validation_messages = fValidationException::removeFieldNames($validation_messages);
			}
			return $validation_messages;
		}
		
		if (!empty($validation_messages)) {
			throw new fValidationException(
				'The following problems were found:',
				$validation_messages
			);
		}
	}
}



/**
 * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fAuthorization.php.



































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
<?php
/**
 * Allows defining and checking user authentication via ACLs, authorization levels or a simple logged in/not logged in scheme
 * 
 * @copyright  Copyright (c) 2007-2011 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fAuthorization
 * 
 * @version    1.0.0b6
 * @changes    1.0.0b6  Fixed ::checkIP() to not trigger a notice when `$_SERVER['REMOTE_ADDR']` is not set [wb, 2011-05-10]
 * @changes    1.0.0b5  Added ::getLoginPage() [wb, 2010-03-09]
 * @changes    1.0.0b4  Updated class to use new fSession API [wb, 2009-10-23]
 * @changes    1.0.0b3  Updated class to use new fSession API [wb, 2009-05-08]
 * @changes    1.0.0b2  Fixed a bug with using named IP ranges in ::checkIP() [wb, 2009-01-10]
 * @changes    1.0.0b   The initial implementation [wb, 2007-06-14]
 */
class fAuthorization
{
	// The following constants allow for nice looking callbacks to static methods
	const addNamedIPRange  = 'fAuthorization::addNamedIPRange';
	const checkACL         = 'fAuthorization::checkACL';
	const checkAuthLevel   = 'fAuthorization::checkAuthLevel';
	const checkIP          = 'fAuthorization::checkIP';
	const checkLoggedIn    = 'fAuthorization::checkLoggedIn';
	const destroyUserInfo  = 'fAuthorization::destroyUserInfo';
	const getLoginPage     = 'fAuthorization::getLoginPage';
	const getRequestedURL  = 'fAuthorization::getRequestedURL';
	const getUserACLs      = 'fAuthorization::getUserACLs';
	const getUserAuthLevel = 'fAuthorization::getUserAuthLevel';
	const getUserToken     = 'fAuthorization::getUserToken';
	const requireACL       = 'fAuthorization::requireACL';
	const requireAuthLevel = 'fAuthorization::requireAuthLevel';
	const requireLoggedIn  = 'fAuthorization::requireLoggedIn';
	const reset            = 'fAuthorization::reset';
	const setAuthLevels    = 'fAuthorization::setAuthLevels';
	const setLoginPage     = 'fAuthorization::setLoginPage';
	const setRequestedURL  = 'fAuthorization::setRequestedURL';
	const setUserACLs      = 'fAuthorization::setUserACLs';
	const setUserAuthLevel = 'fAuthorization::setUserAuthLevel';
	const setUserToken     = 'fAuthorization::setUserToken';
	
	
	/**
	 * The valid auth levels
	 * 
	 * @var array
	 */
	static private $levels = NULL;
	
	/**
	 * The login page
	 * 
	 * @var string
	 */
	static private $login_page = NULL;
	
	/**
	 * Named IP ranges
	 * 
	 * @var array
	 */
	static private $named_ip_ranges = array();
	
	
	/**
	 * Adds a named IP address or range, or array of addresses and/or ranges
	 * 
	 * This method allows ::checkIP() to be called with a name instead of the
	 * actual IPs.
	 * 
	 * @param  string $name       The name to give the IP addresses/ranges
	 * @param  mixed  $ip_ranges  This can be string (or array of strings) of the IPs or IP ranges to restrict to - please see ::checkIP() for format details
	 * @return void
	 */
	static public function addNamedIPRange($name, $ip_ranges)
	{
		self::$named_ip_ranges[$name] = $ip_ranges;
	}
	
	
	/**
	 * Checks to see if the logged in user meets the requirements of the ACL specified
	 * 
	 * @param  string $resource    The resource we are checking permissions for
	 * @param  string $permission  The permission to require from the user
	 * @return boolean  If the user has the required permissions
	 */
	static public function checkACL($resource, $permission)
	{
		if (self::getUserACLs() === NULL) {
			return FALSE;
		}
			
		$acls = self::getUserACLs();
		
		if (!isset($acls[$resource]) && !isset($acls['*'])) {
			return FALSE;
		}
		
		if (isset($acls[$resource])) {
			if (in_array($permission, $acls[$resource]) || in_array('*', $acls[$resource])) {
				return TRUE;
			}
		}
		
		if (isset($acls['*'])) {
			if (in_array($permission, $acls['*']) || in_array('*', $acls['*'])) {
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	
	/**
	 * Checks to see if the logged in user has the specified auth level
	 * 
	 * @param  string $level  The level to check against the logged in user's level
	 * @return boolean  If the user has the required auth level
	 */
	static public function checkAuthLevel($level)
	{
		if (self::getUserAuthLevel()) {
			
			self::validateAuthLevel(self::getUserAuthLevel());
			self::validateAuthLevel($level);
			
			$user_number = self::$levels[self::getUserAuthLevel()];
			$required_number = self::$levels[$level];
			
			if ($user_number >= $required_number) {
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	
	/**
	 * Checks to see if the user is from the IPs or IP ranges specified
	 * 
	 * The `$ip_ranges` parameter can be either a single string, or an array of
	 * strings, each of which should be in one of the following formats:
	 *  
	 *  - A single IP address:
	 *   - 192.168.1.1
	 *   - 208.77.188.166
	 *  - A CIDR range
	 *   - 192.168.1.0/24
	 *   - 208.77.188.160/28
	 *  - An IP/subnet mask combination
	 *   - 192.168.1.0/255.255.255.0
	 *   - 208.77.188.160/255.255.255.240
	 * 
	 * @param  mixed $ip_ranges  A string (or array of strings) of the IPs or IP ranges to restrict to - see method description for details
	 * @return boolean  If the user is coming from (one of) the IPs or ranges specified
	 */
	static public function checkIP($ip_ranges)
	{
		// Check to see if a named IP range was specified
		if (is_string($ip_ranges) && isset(self::$named_ip_ranges[$ip_ranges])) {
			$ip_ranges = self::$named_ip_ranges[$ip_ranges];
		}
		
		if (!isset($_SERVER['REMOTE_ADDR'])) {
			return FALSE;
		}

		// Get the remote IP and remove any IPv6 to IPv4 mapping
		$user_ip      = str_replace('::ffff:', '', $_SERVER['REMOTE_ADDR']);
		$user_ip_long = ip2long($user_ip);
		
		settype($ip_ranges, 'array');
		
		foreach ($ip_ranges as $ip_range) {
			
			if (strpos($ip_range, '/') === FALSE) {
				$ip_range .= '/32';
			}
			
			list($range_ip, $range_mask) = explode('/', $ip_range);
			
			if (strlen($range_mask) < 3) {
				$mask_long = pow(2, 32) - pow(2, 32 - $range_mask);
			} else {
				$mask_long = ip2long($range_mask);
			}
			
			$range_ip_long = ip2long($range_ip);
			
			if (($range_ip_long & $mask_long) != $range_ip_long) {
				$proper_range_ip = long2ip($range_ip_long & $mask_long);
				throw new fProgrammerException(
					'The range base IP address specified, %1$s, is invalid for the CIDR range or subnet mask provided (%2$s). The proper IP is %3$s.',
					$range_ip,
					'/' . $range_mask,
					$proper_range_ip
				);
			}
			
			if (($user_ip_long & $mask_long) == $range_ip_long) {
				return TRUE;
			}
		}
		
		return FALSE;
	}
	
	
	/**
	 * Checks to see if the user has an auth level or ACLs defined
	 * 
	 * @return boolean  If the user is logged in
	 */
	static public function checkLoggedIn()
	{
		if (fSession::get(__CLASS__ . '::user_auth_level', NULL) !== NULL ||
			fSession::get(__CLASS__ . '::user_acls', NULL) !== NULL ||
			fSession::get(__CLASS__ . '::user_token', NULL) !== NULL) {
			return TRUE;
		}
		return FALSE;
	}
	
	
	/**
	 * Destroys the user's auth level and/or ACLs
	 * 
	 * @return void
	 */
	static public function destroyUserInfo()
	{
		fSession::delete(__CLASS__ . '::user_auth_level');
		fSession::delete(__CLASS__ . '::user_acls');
		fSession::delete(__CLASS__ . '::user_token');
		fSession::delete(__CLASS__ . '::requested_url');
	}
	
	
	/**
	 * Returns the login page set via ::setLoginPage()
	 * 
	 * @return string  The login page users are redirected to if they don't have the required authorization
	 */
	static public function getLoginPage()
	{
		return self::$login_page;
	}
	
	/**
	 * Returns the URL requested before the user was redirected to the login page
	 * 
	 * @param  boolean $clear        If the requested url should be cleared from the session after it is retrieved
	 * @param  string  $default_url  The default URL to return if the user was not redirected
	 * @return string  The URL that was requested before they were redirected to the login page
	 */
	static public function getRequestedURL($clear, $default_url=NULL)
	{
		$requested_url = fSession::get(__CLASS__ . '::requested_url', $default_url);
		if ($clear) {
			fSession::delete(__CLASS__ . '::requested_url');
		}
		return $requested_url;
	}
	
	
	/**
	 * Gets the ACLs for the logged in user
	 * 
	 * @return array  The logged in user's ACLs
	 */
	static public function getUserACLs()
	{
		return fSession::get(__CLASS__ . '::user_acls', NULL);
	}
	
	
	/**
	 * Gets the authorization level for the logged in user
	 * 
	 * @return string  The logged in user's auth level
	 */
	static public function getUserAuthLevel()
	{
		return fSession::get(__CLASS__ . '::user_auth_level', NULL);
	}
	
	
	/**
	 * Gets the value that was set as the user token, `NULL` if no token has been set
	 * 
	 * @return mixed  The user token that had been set, `NULL` if none
	 */
	static public function getUserToken()
	{
		return fSession::get(__CLASS__ . '::user_token', NULL);
	}
	
	
	/**
	 * Redirects the user to the login page
	 * 
	 * @return void
	 */
	static private function redirect()
	{
		self::setRequestedURL(fURL::getWithQueryString());
		fURL::redirect(self::$login_page);
	}
	
	
	/**
	 * Redirect the user to the login page if they do not have the permissions required
	 * 
	 * @param  string $resource    The resource we are checking permissions for
	 * @param  string $permission  The permission to require from the user
	 * @return void
	 */
	static public function requireACL($resource, $permission)
	{
		self::validateLoginPage();
		
		if (self::checkACL($resource, $permission)) {
			return;
		}
		
		self::redirect();
	}
	
	
	/**
	 * Redirect the user to the login page if they do not have the auth level required
	 * 
	 * @param  string $level  The level to check against the logged in user's level
	 * @return void
	 */
	static public function requireAuthLevel($level)
	{
		self::validateLoginPage();
		
		if (self::checkAuthLevel($level)) {
			return;
		}
		
		self::redirect();
	}
	
	
	/**
	 * Redirect the user to the login page if they do not have an auth level or ACLs
	 * 
	 * @return void
	 */
	static public function requireLoggedIn()
	{
		self::validateLoginPage();
		
		if (self::checkLoggedIn()) {
			return;
		}
		
		self::redirect();
	}
	
	
	/**
	 * Resets the configuration of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		self::$level           = NULL;
		self::$login_page      = NULL;
		self::$named_ip_ranges = array();
	}
	
	
	/**
	 * Sets the authorization levels to use for level checking
	 * 
	 * @param  array $levels  An associative array of `(string) {level} => (integer) {value}`, for each level
	 * @return void
	 */
	static public function setAuthLevels($levels)
	{
		self::$levels = $levels;
	}
	
	
	/**
	 * Sets the login page to redirect users to
	 * 
	 * @param  string $url  The URL of the login page
	 * @return void
	 */
	static public function setLoginPage($url)
	{
		self::$login_page = $url;
	}
	
	
	/**
	 * Sets the restricted URL requested by the user
	 * 
	 * @param  string  $url  The URL to save as the requested URL
	 * @return void
	 */
	static public function setRequestedURL($url)
	{
		fSession::set(__CLASS__ . '::requested_url', $url);
	}
	
	
	/**
	 * Sets the ACLs for the logged in user.
	 * 
	 * Array should be formatted like:
	 * 
	 * {{{
	 * array (
	 *     (string) {resource name} => array(
	 *         (mixed) {permission}, ...
	 *     ), ...
	 * )
	 * }}}
	 * 
	 * The resource name or the permission may be the single character `'*'`
	 * which acts as a wildcard.
	 * 
	 * @param  array $acls  The logged in user's ACLs - see method description for format
	 * @return void
	 */
	static public function setUserACLs($acls)
	{
		fSession::set(__CLASS__ . '::user_acls', $acls);
		fSession::regenerateID();
	}
	
	
	/**
	 * Sets the authorization level for the logged in user
	 * 
	 * @param  string $level  The logged in user's auth level
	 * @return void
	 */
	static public function setUserAuthLevel($level)
	{
		self::validateAuthLevel($level);
		fSession::set(__CLASS__ . '::user_auth_level', $level);
		fSession::regenerateID();
	}
	
	
	/**
	 * Sets some piece of information to use to identify the current user
	 * 
	 * @param  mixed $token  The user's token. This could be a user id, an email address, a user object, etc.
	 * @return void
	 */
	static public function setUserToken($token)
	{
		fSession::set(__CLASS__ . '::user_token', $token);
		fSession::regenerateID();
	}
	
	
	/**
	 * Makes sure auth levels have been set, and that the specified auth level is valid
	 * 
	 * @param  string $level  The level to validate
	 * @return void
	 */
	static private function validateAuthLevel($level=NULL)
	{
		if (self::$levels === NULL) {
			throw new fProgrammerException(
				'No authorization levels have been set, please call %s',
				__CLASS__ . '::setAuthLevels()'
			);
		}
		if ($level !== NULL && !isset(self::$levels[$level])) {
			throw new fProgrammerException(
				'The authorization level specified, %1$s, is invalid. Must be one of: %2$s.',
				$level,
				join(', ', array_keys(self::$levels))
			);
		}
	}
	
	
	/**
	 * Makes sure a login page has been defined
	 * 
	 * @return void
	 */
	static private function validateLoginPage()
	{
		if (self::$login_page === NULL) {
			throw new fProgrammerException(
				'No login page has been set, please call %s',
				__CLASS__ . '::setLoginPage()'
			);
		}
	}
	
	
	/**
	 * Forces use as a static class
	 * 
	 * @return fAuthorization
	 */
	private function __construct() { }
}



/**
 * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fAuthorizationException.php.



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
/**
 * An exception caused by an authorization error
 * 
 * @copyright  Copyright (c) 2011 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fConnectivityException
 * 
 * @version    1.0.0b
 * @changes    1.0.0b  The initial implementation [wb, 2011-05-09]
 */
class fAuthorizationException extends fExpectedException
{
}



/**
 * Copyright (c) 2011 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fBuffer.php.



























































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
<?php
/**
 * Provides a single, simplified interface for [http://php.net/outcontrol output buffering] to prevent nested buffering issues and provide a more logical API
 * 
 * @copyright  Copyright (c) 2008-2010 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fBuffer
 * 
 * @version    1.0.0b3
 * @changes    1.0.0b3  Added a check to ensure the zlib extension is installd when doing gzipped buffering [wb, 2010-05-20]
 * @changes    1.0.0b2  Added the `$gzip` parameter to ::start() [wb, 2010-05-19]
 * @changes    1.0.0b   The initial implementation [wb, 2008-03-16]
 */
class fBuffer
{
	// The following constants allow for nice looking callbacks to static methods
	const erase        = 'fBuffer::erase';
	const get          = 'fBuffer::get';
	const isStarted    = 'fBuffer::isStarted';
	const replace      = 'fBuffer::replace';
	const reset        = 'fBuffer::reset';
	const start        = 'fBuffer::start';
	const startCapture = 'fBuffer::startCapture';
	const stop         = 'fBuffer::stop';
	const stopCapture  = 'fBuffer::stopCapture';
	
	
	/**
	 * If output capturing is currently active
	 * 
	 * @var boolean
	 */
	static private $capturing = FALSE;
	
	/**
	 * If output buffering has been started
	 * 
	 * @var integer
	 */
	static private $started = FALSE;
	
	
	/**
	 * Erases the output buffer
	 * 
	 * @return void
	 */
	static public function erase()
	{
		if (!self::$started) {
			throw new fProgrammerException(
				'The output buffer can not be erased since output buffering has not been started'
			);
		}
		if (self::$capturing) {
			throw new fProgrammerException(
				'Output capturing is currently active and it must be stopped before the buffer can be erased'
			);
		}
		ob_clean();
	}
	
	
	/**
	 * Returns the contents of output buffer
	 * 
	 * @return string  The contents of the output buffer
	 */
	static public function get()
	{
		if (!self::$started) {
			throw new fProgrammerException(
				'The output buffer can not be retrieved because it has not been started'
			);
		}
		if (self::$capturing) {
			throw new fProgrammerException(
				'Output capturing is currently active and it must be stopped before the buffer can be retrieved'
			);
		}
		return ob_get_contents();
	}
	
	
	/**
	 * Checks if buffering has been started
	 * 
	 * @return boolean  If buffering has been started
	 */
	static public function isStarted()
	{
		return self::$started;
	}
	
	
	/**
	 * Replaces a value in the output buffer
	 * 
	 * @param  string $find     The string to find
	 * @param  string $replace  The string to replace
	 * @return void
	 */
	static public function replace($find, $replace)
	{
		if (!self::$started) {
			throw new fProgrammerException(
				'A replacement can not be made since output buffering has not been started'
			);
		}
		if (self::$capturing) {
			throw new fProgrammerException(
				'Output capturing is currently active and it must be stopped before a replacement can be made'
			);
		}
		
		// ob_get_clean() actually turns off output buffering, so we do it the long way
		$contents = ob_get_contents();
		ob_clean();
		
		echo str_replace($find, $replace, $contents);
	}
	
	
	/**
	 * Resets the configuration and buffer of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		if (self::$capturing) {
			self::stopCapture();	
		}
		if (self::$started) {
			self::erase();
			self::stop();	
		}
	}
	
	
	/**
	 * Starts output buffering
	 * 
	 * @param  boolean $gzip  If the buffered output should be gzipped using [http://php.net/ob_gzhandler `ob_gzhandler()`]
	 * @return void
	 */
	static public function start($gzip=FALSE)
	{
		if (self::$started) {
			throw new fProgrammerException(
				'Output buffering has already been started'
			);
		}
		if (self::$capturing) {
			throw new fProgrammerException(
				'Output capturing is currently active and it must be stopped before the buffering can be started'
			);
		}
		if ($gzip && !extension_loaded('zlib')) {
			throw new fEnvironmentException(
				'The PHP %s extension is required for gzipped buffering, however is does not appear to be loaded',
				'zlib'
			);
		}
		ob_start($gzip ? 'ob_gzhandler' : NULL);
		self::$started = TRUE;
	}
	
	
	/**
	 * Starts capturing output, should be used with ::stopCapture() to grab output from code that does not offer an option of returning a value instead of outputting it
	 * 
	 * @return void
	 */
	static public function startCapture()
	{
		if (self::$capturing) {
			throw new fProgrammerException(
				'Output capturing has already been started'
			);
		}
		ob_start();
		self::$capturing = TRUE;
	}
	
	
	/**
	 * Stops output buffering, flushing everything to the browser
	 * 
	 * @return void
	 */
	static public function stop()
	{
		if (!self::$started) {
			throw new fProgrammerException(
				'Output buffering can not be stopped since it has not been started'
			);
		}
		if (self::$capturing) {
			throw new fProgrammerException(
				'Output capturing is currently active and it must be stopped before buffering can be stopped'
			);
		}
		
		// Only flush if there is content to push out, otherwise
		// we might prevent headers from being sent
		if (ob_get_contents()) {
			ob_end_flush();
		} else {
			ob_end_clean();
		}
		
		self::$started = FALSE;
	}
	
	
	/**
	 * Stops capturing output, returning what was captured
	 * 
	 * @return string  The captured output
	 */
	static public function stopCapture()
	{
		if (!self::$capturing) {
			throw new fProgrammerException(
				'Output capturing can not be stopped since it has not been started'
			);
		}
		self::$capturing = FALSE;
		return ob_get_clean();
	}
	
	
	/**
	 * Forces use as a static class
	 * 
	 * @return fBuffer
	 */
	private function __construct() { }
}



/**
 * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fCRUD.php.











































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
<?php
/**
 * Provides miscellaneous functionality for [http://en.wikipedia.org/wiki/Create,_read,_update_and_delete CRUD-like] pages
 * 
 * @copyright  Copyright (c) 2007-2009 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fCRUD
 * 
 * @version    1.0.0b5
 * @changes    1.0.0b5  Updated class to use new fSession API [wb, 2009-10-23]
 * @changes    1.0.0b4  Updated class to use new fSession API [wb, 2009-05-08]
 * @changes    1.0.0b3  Backwards Compatiblity Break - moved ::printOption() to fHTML::printOption(), ::showChecked() to fHTML::showChecked(), ::removeListItems() and ::reorderListItems() to fException::splitMessage(), ::generateRequestToken() to fRequest::generateCSRFToken(), and ::validateRequestToken() to fRequest::validateCSRFToken() [wb, 2009-05-08]
 * @changes    1.0.0b2  Fixed a bug preventing loaded search values from being included in redirects [wb, 2009-03-18]
 * @changes    1.0.0b   The initial implementation [wb, 2007-06-14]
 */
class fCRUD
{
	// The following constants allow for nice looking callbacks to static methods
	const getColumnClass           = 'fCRUD::getColumnClass';
	const getRowClass              = 'fCRUD::getRowClass';
	const getSearchValue           = 'fCRUD::getSearchValue';
	const getSortColumn            = 'fCRUD::getSortColumn';
	const getSortDirection         = 'fCRUD::getSortDirection';
	const printSortableColumn      = 'fCRUD::printSortableColumn';
	const redirectWithLoadedValues = 'fCRUD::redirectWithLoadedValues';
	const reset                    = 'fCRUD::reset';
	
	
	/**
	 * Any values that were loaded from the session, used for redirection
	 * 
	 * @var array
	 */
	static private $loaded_values = array();
	
	/**
	 * The current row number for alternating rows
	 * 
	 * @var integer
	 */
	static private $row_number = 1;
	
	/**
	 * The values for a search form
	 * 
	 * @var array
	 */
	static private $search_values = array();
	
	/**
	 * The column to sort by
	 * 
	 * @var string
	 */
	static private $sort_column = NULL;
	
	/**
	 * The direction to sort
	 * 
	 * @var string
	 */
	static private $sort_direction = NULL;
	
	
	/**
	 * Return the string `'sorted'` if `$column` is the column that is currently being sorted by, otherwise returns `''`
	 * 
	 * This method will only be useful if used with the other sort methods 
	 * ::printSortableColumn(), ::getSortColumn() and ::getSortDirection(). 
	 * 
	 * @param  string $column  The column to check
	 * @return string  The CSS class for the column, either `''` or `'sorted'`
	 */
	static public function getColumnClass($column)
	{
		if (self::$sort_column == $column) {
			return 'sorted';
		}
		return '';
	}
	
	
	/**
	 * Returns the previous values for the specified search field
	 * 
	 * @param  string $column  The column to get the value for
	 * @return mixed  The previous value
	 */
	static private function getPreviousSearchValue($column)
	{
		return fSession::get(__CLASS__ . '::' . fURL::get() . '::previous_search::' . $column, NULL);
	}
	
	
	/**
	 * Return the previous sort column, if one exists
	 * 
	 * @return string  The previous sort column
	 */
	static private function getPreviousSortColumn()
	{
		return fSession::get(__CLASS__ . '::' . fURL::get() . '::previous_sort_column', NULL);
	}
	
	
	/**
	 * Return the previous sort direction, if one exists
	 * 
	 * @return string  The previous sort direction
	 */
	static private function getPreviousSortDirection()
	{
		return fSession::get(__CLASS__ . '::' . fURL::get() . '::previous_sort_direction', NULL);
	}
	
	
	/**
	 * Returns a CSS class name for a row
	 * 
	 * Will return `'even'`, `'odd'`, or `'highlighted'` if the two parameters
	 * are equal and not `NULL`. The first call to this method will return
	 * the appropriate class concatenated with `' first'`.
	 * 
	 * @param  mixed $row_value       The value from the row
	 * @param  mixed $affected_value  The value that was just added or updated
	 * @return string  The css class
	 */
	static public function getRowClass($row_value=NULL, $affected_value=NULL)
	{
		if ($row_value !== NULL && $row_value == $affected_value) {
			 self::$row_number++;
			 $class = 'highlighted';
		} else {
			$class = (self::$row_number++ % 2) ? 'odd' : 'even';
		}
		
		$class .= (self::$row_number == 2) ? ' first' : '';
		return $class;
	}
	
	
	/**
	 * Gets the current value of a search field
	 * 
	 * If a value is an empty string and no cast to is specified, the value will
	 * become `NULL`.
	 * 
	 * If a query string of `?reset` is passed, all previous search values will
	 * be erased.
	 * 
	 * @param  string $column   The column that is being pulled back
	 * @param  string $cast_to  The data type to cast to
	 * @param  string $default  The default value
	 * @return mixed  The current value
	 */
	static public function getSearchValue($column, $cast_to=NULL, $default=NULL)
	{
		// Reset values if requested
		if (self::wasResetRequested()) {
			self::setPreviousSearchValue($column, NULL);
			return;
		}
		
		if (self::getPreviousSearchValue($column) && !fRequest::check($column)) {
			self::$search_values[$column] = self::getPreviousSearchValue($column);
			self::$loaded_values[$column] = self::$search_values[$column];
		} else {
			self::$search_values[$column] = fRequest::get($column, $cast_to, $default);
			self::setPreviousSearchValue($column, self::$search_values[$column]);
		}
		return self::$search_values[$column];
	}
	
	
	/**
	 * Gets the current column to sort by, defaults to first one specified
	 * 
	 * @param  string $possible_column  The columns that can be sorted by, defaults to first
	 * @param  string ...
	 * @return string  The column to sort by
	 */
	static public function getSortColumn($possible_column)
	{
		// Reset value if requested
		if (self::wasResetRequested()) {
			self::setPreviousSortColumn(NULL);
			return;
		}
		
		$possible_columns = func_get_args();
		
		if (sizeof($possible_columns) == 1 && is_array($possible_columns[0])) {
			$possible_columns = $possible_columns[0];
		}
		
		if (self::getPreviousSortColumn() && !fRequest::check('sort')) {
			self::$sort_column = self::getPreviousSortColumn();
			self::$loaded_values['sort'] = self::$sort_column;
		} else {
			self::$sort_column = fRequest::getValid('sort', $possible_columns);
			self::setPreviousSortColumn(self::$sort_column);
		}
		return self::$sort_column;
	}
	
	
	/**
	 * Gets the current sort direction
	 * 
	 * @param  string $default_direction  The default direction, `'asc'` or `'desc'`
	 * @return string  The direction, `'asc'` or `'desc'`
	 */
	static public function getSortDirection($default_direction)
	{
		// Reset value if requested
		if (self::wasResetRequested()) {
			self::setPreviousSortDirection(NULL);
			return;
		}
		
		if (self::getPreviousSortDirection() && !fRequest::check('dir')) {
			self::$sort_direction = self::getPreviousSortDirection();
			self::$loaded_values['dir'] = self::$sort_direction;
		} else {
			self::$sort_direction = fRequest::getValid('dir', array($default_direction, ($default_direction == 'asc') ? 'desc' : 'asc'));
			self::setPreviousSortDirection(self::$sort_direction);
		}
		return self::$sort_direction;
	}
	
	
	/**
	 * Prints a sortable column header `a` tag
	 * 
	 * The a tag will include the CSS class `'sortable_column'` and the
	 * direction being sorted, `'asc'` or `'desc'`.
	 * 
	 * {{{
	 * #!php
	 * fCRUD::printSortableColumn('name', 'Name');
	 * }}}
	 * 
	 * would create the following HTML based on the page context
	 * 
	 * {{{
	 * #!html
	 * <!-- If name is the current sort column in the asc direction, the output would be -->
	 * <a href="?sort=name&dir=desc" class="sorted_column asc">Name</a>
	 * 
	 * <!-- If name is not the current sort column, the output would be -->
	 * <a href="?sort-name&dir=asc" class="sorted_column">Name</a>
	 * }}}
	 * 
	 * @param  string $column       The column to create the sortable header for
	 * @param  string $column_name  This will override the humanized version of the column
	 * @return void
	 */
	static public function printSortableColumn($column, $column_name=NULL)
	{
		if ($column_name === NULL) {
			$column_name = fGrammar::humanize($column);
		}
		
		if (self::$sort_column == $column) {
			$sort      = $column;
			$direction = (self::$sort_direction == 'asc') ? 'desc' : 'asc';
		} else {
			$sort      = $column;
			$direction = 'asc';
		}
		
		$columns = array_merge(array('sort', 'dir'), array_keys(self::$search_values));
		$values  = array_merge(array($sort, $direction), array_values(self::$search_values));
		
		$url         = fHTML::encode(fURL::get() . fURL::replaceInQueryString($columns, $values));
		$css_class   = (self::$sort_column == $column) ? ' ' . self::$sort_direction : '';
		$column_name = fHTML::prepare($column_name);
		
		echo '<a href="' . $url . '" class="sortable_column' . $css_class . '">' . $column_name . '</a>';
	}
		
	
	/**
	 * Checks to see if any values (search or sort) were loaded from the session, and if so redirects the user to the current URL with those values added
	 * 
	 * @return void
	 */
	static public function redirectWithLoadedValues()
	{
		// If values were reset, redirect to the plain URL
		if (self::wasResetRequested()) {
			fURL::redirect(fURL::get() . fURL::removeFromQueryString('reset'));
		}
		
		$query_string = fURL::replaceInQueryString(array_keys(self::$loaded_values), array_values(self::$loaded_values));
		$url = fURL::get() . $query_string;
		
		if ($url != fURL::getWithQueryString() && $url != fURL::getWithQueryString() . '?') {
			fURL::redirect($url);
		}
	}
	
	
	/**
	 * Resets the configuration and data of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		fSession::clear(__CLASS__ . '::');
		
		self::$loaded_values  = array();
		self::$row_number     = 1;
		self::$search_values  = array();
		self::$sort_column    = NULL;
		self::$sort_direction = NULL;
	}
	
	
	/**
	 * Sets a value for a search field
	 * 
	 * @param  string $column  The column to save the value for
	 * @param  mixed  $value   The value to save
	 * @return void
	 */
	static private function setPreviousSearchValue($column, $value)
	{
		fSession::set(__CLASS__ . '::' . fURL::get() . '::previous_search::' . $column, $value);
	}
	
	
	/**
	 * Set the sort column to be used on returning pages
	 * 
	 * @param  string $sort_column  The sort column to save
	 * @return void
	 */
	static private function setPreviousSortColumn($sort_column)
	{
		fSession::set(__CLASS__ . '::' . fURL::get() . '::previous_sort_column', $sort_column);
	}
	
	
	/**
	 * Set the sort direction to be used on returning pages
	 * 
	 * @param  string $sort_direction  The sort direction to save
	 * @return void
	 */
	static private function setPreviousSortDirection($sort_direction)
	{
		fSession::set(__CLASS__ . '::' . fURL::get() . '::previous_sort_direction', $sort_direction);
	}
	
	
	/**
	 * Indicates if a reset was requested for search values
	 * 
	 * @return boolean  If a reset was requested
	 */
	static private function wasResetRequested()
	{
		$tail = substr(fURL::getWithQueryString(), -6);
		return $tail == '?reset' || $tail == '&reset';
	}
	
	
	/**
	 * Prevent instantiation
	 * 
	 * @return fCRUD
	 */
	private function __construct() { }
}



/**
 * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fCache.php.



















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
<?php
/**
 * A simple interface to cache data using different backends
 * 
 * @copyright  Copyright (c) 2009-2012 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fCache
 * 
 * @version    1.0.0b6
 * @changes    1.0.0b6  Fixed a bug with ::add() setting a value when it shouldn't if no ttl was given for the file backend [wb, 2012-01-12]
 * @changes    1.0.0b5  Added missing documentation for using Redis as a backend [wb, 2011-08-25]
 * @changes    1.0.0b4  Added the `database`, `directory` and `redis` types, added support for the memcached extention and support for custom serialization callbacks [wb, 2011-06-21]
 * @changes    1.0.0b3  Added `0` to the memcache delete method call since otherwise the method triggers notices on some installs [wb, 2011-05-10]
 * @changes    1.0.0b2  Fixed API calls to the memcache extension to pass the TTL as the correct parameter [wb, 2011-02-01]
 * @changes    1.0.0b   The initial implementation [wb, 2009-04-28]
 */
class fCache
{
	/**
	 * The cache configuration, used for database, directory and file caches
	 * 
	 * The array structure for database caches is:
	 * {{{
	 * array(
	 *     'table'        => (string) {the database table to use},
	 *     'key_column'   => (string) {the varchar column to store the key in, should be able to handle at least 250 characters},
	 *     'value_column' => (string) {the text/varchar column to store the value in},
	 *     'ttl_column'   => (string) {the integer column to store the expiration time in}
	 * )
	 * }}}
	 *
	 * The array structure for directory caches:
	 * {{{
	 * array(
	 *     'path' => (string) {the directory path with trailing slash}
	 * )
	 * }}}
	 *
	 * The array structure for file caches:
	 * {{{
	 * array(
	 *     'path'  => (string) {the file path},
	 *     'state' => (string) {clean or dirty, used to appropriately save}
	 * )
	 * }}}
	 * 
	 * @var array
	 */
	protected $config;
	
	/**
	 * The data store to use
	 *
	 * Either the:
	 *  - array structure for file cache
	 *  - Memcache or Memcached object for memcache
	 *  - fDatabase object for database
	 *  - Redis object for redis
	 *
	 * Not used for apc, directory or xcache
	 * 
	 * @var mixed
	 */
	protected $data_store;

	/**
	 * The type of cache
	 * 
	 * The valid values are:
	 *  - `'apc'`
	 *  - `'database'`
	 *  - `'directory'`
	 *  - `'file'`
	 *  - `'memcache'`
	 *  - `'redis'`
	 *  - `'xcache'`
	 * 
	 * @var string
	 */
	protected $type;
	
	/**
	 * Set the type and master key for the cache
	 * 
	 * A `file` cache uses a single file to store values in an associative
	 * array and is probably not suitable for a large number of keys.
	 * 
	 * Using an `apc` or `xcache` cache will have far better performance
	 * than a file or directory, however please remember that keys are shared
	 * server-wide.
	 *
	 * `$config` is an associative array of configuration options for the various
	 * backends. Some backend require additional configuration, while others
	 * provide provide optional settings.
	 *
	 * The following `$config` options must be set for the `database` backend:
	 *
	 *  - `table`: The database table to use for caching
	 *  - `key_column`: The column to store the cache key in - must support at least 250 character strings
	 *  - `value_column`: The column to store the serialized value in - this should probably be a `TEXT` column to support large values, or `BLOB` if binary serialization is used
	 *  - `value_data_type`: If a `BLOB` column is being used for the `value_column`, this should be set to 'blob', otherwise `string`
	 *  - `ttl_column`: The column to store the expiration timestamp of the cached entry - this should be an integer
	 *
	 * The following `$config` for the following items can be set for all backends:
	 * 
	 *  - `serializer`: A callback to serialize data with, defaults to the PHP function `serialize()`
	 *  - `unserializer`: A callback to unserialize data with, defaults to the PHP function `unserialize()`
	 *
	 * Common serialization callbacks include:
	 * 
	 *  - `json_encode`/`json_decode`
	 *  - `igbinary_serialize`/`igbinary_unserialize`
	 *
	 * Please note that using JSON for serialization will exclude all non-public
	 * properties of objects from being serialized.
	 *
	 * A custom `serialize` and `unserialze` option is `string`, which will cast
	 * all values to a string when storing, instead of serializing them. If a
	 * `__toString()` method is provided for objects, it will be called. 
	 * 
	 * @param  string $type        The type of caching to use: `'apc'`, `'database'`, `'directory'`, `'file'`, `'memcache'`, `'redis'`, `'xcache'`
	 * @param  mixed  $data_store  The path for a `file` or `directory` cache, an `Memcache` or `Memcached` object for a `memcache` cache, an fDatabase object for a `database` cache or a `Redis` object for a `redis` cache - not used for `apc` or `xcache`
	 * @param  array  $config      Configuration options - see method description for details
	 * @return fCache
	 */
	public function __construct($type, $data_store=NULL, $config=array())
	{
		switch ($type) {
			case 'database': 
				foreach (array('table', 'key_column', 'value_column', 'ttl_column') as $key) {
					if (empty($config[$key])) {
						throw new fProgrammerException(
							'The config key %s is not set',
							$key
						);
					}
				}
				$this->config = $config;
				if (!isset($this->config['value_data_type'])) {
					$this->config['value_data_type'] = 'string';
				}
				if (!$data_store instanceof fDatabase) {
					throw new fProgrammerException(
						'The data store provided is not a valid %s object',
						'fDatabase'
					);
				}
				$this->data_store = $data_store;
				break;

			case 'directory': 
				$exists = file_exists($data_store);
				if (!$exists) {
					throw new fEnvironmentException(
						'The directory specified, %s, does not exist',
						$data_store
					);		
				}
				if (!is_dir($data_store)) {
					throw new fEnvironmentException(
						'The path specified, %s, is not a directory',
						$data_store
					);		
				}
				if (!is_writable($data_store)) {
					throw new fEnvironmentException(
						'The directory specified, %s, is not writable',
						$data_store
					);		
				}
				$this->config['path'] = realpath($data_store) . DIRECTORY_SEPARATOR;
				break;

			case 'file': 
				$exists = file_exists($data_store);
				if (!$exists && !is_writable(dirname($data_store))) {
					throw new fEnvironmentException(
						'The file specified, %s, does not exist and the directory it in inside of is not writable',
						$data_store
					);		
				}
				if ($exists && !is_writable($data_store)) {
					throw new fEnvironmentException(
						'The file specified, %s, is not writable',
						$data_store
					);
				}
				$this->config['path'] = $data_store;
				if ($exists) {
					$this->data_store = unserialize(file_get_contents($data_store));
				} else {
					$this->data_store = array();	
				}
				$this->config['state'] = 'clean';
				break;

			case 'memcache':
				if (!$data_store instanceof Memcache && !$data_store instanceof Memcached) {
					throw new fProgrammerException(
						'The data store provided is not a valid %s or %s object',
						'Memcache',
						'Memcached'
					);
				}
				$this->data_store = $data_store;
				break;
			
			case 'redis':
				if (!$data_store instanceof Redis) {
					throw new fProgrammerException(
						'The data store provided is not a valid %s object',
						'Redis'
					);
				}
				$this->data_store = $data_store;
				break;

			case 'apc':
			case 'xcache':
				if (!extension_loaded($type)) {
					throw new fEnvironmentException(
						'The %s extension does not appear to be installed',
						$type
					);	
				}
				break;
				
			default:
				throw new fProgrammerException(
					'The type specified, %s, is not a valid cache type. Must be one of: %s.',
					$type,
					join(', ', array('apc', 'database', 'directory', 'file', 'memcache', 'redis', 'xcache'))
				);	
		}

		$this->config['serializer']   = isset($config['serializer'])   ? $config['serializer']   : 'serialize';
		$this->config['unserializer'] = isset($config['unserializer']) ? $config['unserializer'] : 'unserialize';

		$this->type = $type;				
	}
	
	
	/**
	 * Cleans up after the cache object
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	public function __destruct()
	{
		// Only sometimes clean the cache of expired values
		if (rand(0, 99) == 50) {
			$this->clean();
		}

		$this->save();
	}
	
	
	/**
	 * Tries to set a value to the cache, but stops if a value already exists
	 * 
	 * @param  string  $key    The key to store as, this should not exceed 250 characters
	 * @param  mixed   $value  The value to store, this will be serialized
	 * @param  integer $ttl    The number of seconds to keep the cache valid for, 0 for no limit
	 * @return boolean  If the key/value pair were added successfully
	 */
	public function add($key, $value, $ttl=0)
	{
		$value = $this->serialize($value);

		switch ($this->type) {
			case 'apc':
				return apc_add($key, $value, $ttl);
				
			case 'file':
				if (isset($this->data_store[$key]) && (($this->data_store[$key]['expire'] && $this->data_store[$key]['expire'] >= time()) || !$this->data_store[$key]['expire'])) {
					return FALSE;	
				}
				$this->data_store[$key] = array(
					'value'  => $value,
					'expire' => (!$ttl) ? 0 : time() + $ttl
				);
				$this->config['state'] = 'dirty';
				return TRUE;
			
			case 'database':
				$res = $this->data_store->query(
					"SELECT %r FROM %r WHERE %r = %s",
					$this->config['key_column'],
					$this->config['table'],
					$this->config['key_column'],
					$key
				);
				if ($res->countReturnedRows()) {
					return FALSE;
				}
				try {
					$value_placeholder = $this->config['value_data_type'] == 'blob' ? '%l' : '%s';
					$this->data_store->query(
						"INSERT INTO %r (%r, %r, %r) VALUES (%s, " . $value_placeholder . ", %i)",
						$this->config['table'],
						$this->config['key_column'],
						$this->config['value_column'],
						$this->config['ttl_column'],
						$key,
						$value,
						(!$ttl) ? 0 : time() + $ttl
					);
					return TRUE;
				} catch (fSQLException $e) {
					return FALSE;
				}

			case 'directory':
				if (file_exists($this->config['path'] . $key)) {
					return FALSE;
				}
				$expiration_date = (!$ttl) ? 0 : time() + $ttl;
				file_put_contents(
					$this->config['path'] . $key,
					$expiration_date . "\n" . $value
				);
				return TRUE;
			
			case 'memcache':
				if ($ttl > 2592000) {
					$ttl = time() + 2592000;		
				}
				if ($this->data_store instanceof Memcache) {
					return $this->data_store->add($key, $value, 0, $ttl);
				}
				return $this->data_store->add($key, $value, $ttl);
			
			case 'redis':
				if (!$ttl) {
					return $this->data_store->setnx($key, $value);
				}
				if ($this->data_store->exists($key)) {
					return FALSE;
				}
				$this->data_store->setex($key, $ttl, $value);
				return TRUE;
			
			case 'xcache':
				if (xcache_isset($key)) {
					return FALSE;	
				}
				xcache_set($key, $value, $ttl);
				return TRUE;
		}		
	}


	/**
	 * Removes all cache entries that have expired
	 *
	 * @return void
	 */
	public function clean()
	{
		switch ($this->type) {
			case 'database':
				$this->data_store->query(
					"DELETE FROM %r WHERE %r != 0 AND %r < %i",
					$this->config['table'],
					$this->config['ttl_column'],
					$this->config['ttl_column'],
					time()
				);
				break;

			case 'directory':
				$clear_before = time();
				$files = array_diff(scandir($this->config['path']), array('.', '..'));
				foreach ($files as $file) {
					if (!file_exists($this->config['path'] . $file)) {
						continue;
					}
					$handle = fopen($this->config['path'] . $file, 'r');
					$expiration_date = trim(fgets($handle));
					fclose($handle);
					if ($expiration_date && $expiration_date < $clear_before) {
						unlink($this->config['path'] . $file);
					}
				}
				break;

			case 'file':
				$clear_before = time();
				foreach ($this->data_store as $key => $value) {
					if ($value['expire'] && $value['expire'] < $clear_before) {
						unset($this->data_store[$key]);	
						$this->config['state'] = 'dirty';
					}	
				}
				break;
		}
	}
	
	
	/**
	 * Clears the WHOLE cache of every key, use with caution!
	 * 
	 * xcache may require a login or password depending on your ini settings.
	 * 
	 * @return boolean  If the cache was successfully cleared
	 */
	public function clear()
	{
		switch ($this->type) {
			case 'apc':
				return apc_clear_cache('user');
			
			case 'database':
				$this->data_store->query(
					"DELETE FROM %r",
					$this->config['table']
				);
				return TRUE;
			
			case 'directory':
				$files = array_diff(scandir($this->config['path']), array('.', '..'));
				$success = TRUE;
				foreach ($files as $file) {
					$success = unlink($this->config['path'] . $file) && $success;
				}
				return $success;
				
			case 'file':
				$this->data_store = array();
				$this->config['state'] = 'dirty';
				return TRUE;
			
			case 'memcache':
				return $this->data_store->flush();
			
			case 'redis':
				return $this->data_store->flushDB();
			
			case 'xcache':
				fCore::startErrorCapture();
				xcache_clear_cache(XC_TYPE_VAR, 0);
				return (bool) fCore::stopErrorCapture();
		}			
	}
	
	
	/**
	 * Deletes a value from the cache
	 * 
	 * @param  string $key  The key to delete
	 * @return boolean  If the delete succeeded
	 */
	public function delete($key)
	{
		switch ($this->type) {
			case 'apc':
				return apc_delete($key);
				
			case 'database':
				return $this->data_store->query(
					"DELETE FROM %r WHERE %r = %s",
					$this->config['table'],
					$this->config['key_column'],
					$key
				)->countAffectedRows();
			
			case 'directory':
				return unlink($this->config['path'] . $key);

			case 'file':
				if (isset($this->data_store[$key])) {
					unset($this->data_store[$key]);
					$this->config['state'] = 'dirty';	
				}
				return TRUE;
			
			case 'memcache':
				return $this->data_store->delete($key, 0);
			
			case 'redis':
				return (bool) $this->data_store->delete($key);
			
			case 'xcache':
				return xcache_unset($key);
		}		
	}
	
	
	/**
	 * Returns a value from the cache
	 * 
	 * @param  string $key      The key to return the value for
	 * @param  mixed  $default  The value to return if the key did not exist
	 * @return mixed  The cached value or the default value if no cached value was found
	 */
	public function get($key, $default=NULL)
	{
		switch ($this->type) {
			case 'apc':
				$value = apc_fetch($key);
				if ($value === FALSE) { return $default; }
				break;

			case 'database':
				$res = $this->data_store->query(
					"SELECT %r FROM %r WHERE %r = %s AND (%r = 0 OR %r >= %i)",
					$this->config['value_column'],
					$this->config['table'],
					$this->config['key_column'],
					$key,
					$this->config['ttl_column'],
					$this->config['ttl_column'],
					time()
				);
				if (!$res->countReturnedRows()) { return $default; }
				$value = $res->fetchScalar();
				break;
			
			case 'directory':
				if (!file_exists($this->config['path'] . $key)) {
					return $default;
				}
				$handle = fopen($this->config['path'] . $key, 'r');
				$expiration_date = fgets($handle);
				if ($expiration_date != 0 && $expiration_date < time()) {
					return $default;
				}
				$value = '';
				while (!feof($handle)) {
					$value .= fread($handle, 524288);
				}
				fclose($handle);
				break;
				
			case 'file':
				if (isset($this->data_store[$key])) {
					$expire = $this->data_store[$key]['expire'];
					if (!$expire || $expire >= time()) {
						$value = $this->data_store[$key]['value'];
					} elseif ($expire) {
						unset($this->data_store[$key]);
						$this->config['state'] = 'dirty';
					}
				} 
				if (!isset($value)) {
					return $default;
				}
				break;
			
			case 'memcache':
				$value = $this->data_store->get($key);
				if ($value === FALSE) { return $default; }
				break;
			
			case 'redis':
				$value = $this->data_store->get($key);
				if ($value === FALSE) { return $default; }
				break;
			
			case 'xcache':
				$value = xcache_get($key);
				if ($value === FALSE) { return $default; }
		}
		
		return $this->unserialize($value);		
	}
	
	
	/**
	 * Only valid for `file` caches, saves the file to disk
	 * 
	 * @return void
	 */
	public function save()
	{
		if ($this->type != 'file' || $this->config['state'] == 'clean') {
			return;
		}
		
		file_put_contents($this->config['path'], serialize($this->data_store));
		$this->config['state'] = 'clean';	
	}


	/**
	 * Serializes a value before storing it in the cache
	 *
	 * @param mixed $value  The value to serialize
	 * @return string  The serialized value
	 */
	protected function serialize($value)
	{
		if ($this->config['serializer'] == 'string') {
			if (is_object($value) && method_exists($value, '__toString')) {
				return $value->__toString();
			}
			return (string) $value;
		}

		return call_user_func($this->config['serializer'], $value);
	}
	
	
	/**
	 * Sets a value to the cache, overriding any previous value
	 * 
	 * @param  string  $key    The key to store as, this should not exceed 250 characters
	 * @param  mixed   $value  The value to store, this will be serialized
	 * @param  integer $ttl    The number of seconds to keep the cache valid for, 0 for no limit
	 * @return boolean  If the value was successfully saved
	 */
	public function set($key, $value, $ttl=0)
	{
		$value = $this->serialize($value);

		switch ($this->type) {
			case 'apc':
				return apc_store($key, $value, $ttl);
				
			case 'database':
				$res = $this->data_store->query(
					"SELECT %r FROM %r WHERE %r = %s",
					$this->config['key_column'],
					$this->config['table'],
					$this->config['key_column'],
					$key
				);

				$expiration_date = (!$ttl) ? 0 : time() + $ttl;

				try {
					$value_placeholder = $this->config['value_data_type'] == 'blob' ? '%l' : '%s';
					if (!$res->countReturnedRows()) {
						$this->data_store->query(
							"INSERT INTO %r (%r, %r, %r) VALUES (%s, " . $value_placeholder . ", %i)",
							$this->config['table'],
							$this->config['key_column'],
							$this->config['value_column'],
							$this->config['ttl_column'],
							$key,
							$value,
							$expiration_date
						);
					} else {
						$this->data_store->query(
							"UPDATE %r SET %r = " . $value_placeholder . ", %r = %s WHERE %r = %s",
							$this->config['table'],
							$this->config['value_column'],
							$value,
							$this->config['ttl_column'],
							$expiration_date,
							$this->config['key_column'],
							$key
						);
					}
				} catch (fSQLException $e) {
					return FALSE;	
				}
				return TRUE;
			
			case 'directory':
				$expiration_date = (!$ttl) ? 0 : time() + $ttl;
				return (bool) file_put_contents(
					$this->config['path'] . $key,
					$expiration_date . "\n" . $value
				);

			case 'file':
				$this->data_store[$key] = array(
					'value'  => $value,
					'expire' => (!$ttl) ? 0 : time() + $ttl
				);
				$this->config['state'] = 'dirty';
				return TRUE;
			
			case 'memcache':
				if ($ttl > 2592000) {
					$ttl = time() + 2592000;
				}
				if ($this->data_store instanceof Memcache) {
					$result = $this->data_store->replace($key, $value, 0, $ttl);
					if (!$result) {
						return $this->data_store->set($key, $value, 0, $ttl);
					}
					return $result;
				}
				return $this->data_store->set($key, $value, $ttl);
			
			case 'redis':
				if ($ttl) {
					return $this->data_store->setex($key, $value, $ttl);
				}
				return $this->data_store->set($key, $value);
			
			case 'xcache':
				return xcache_set($key, $value, $ttl);
		}				
	}


	/**
	 * Unserializes a value before returning it
	 *
	 * @param string $value  The serialized value
	 * @return mixed  The PHP value
	 */
	protected function unserialize($value)
	{
		if ($this->config['unserializer'] == 'string') {
			return $value;
		}

		return call_user_func($this->config['unserializer'], $value);
	}
}



/**
 * Copyright (c) 2009-2012 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fConnectivityException.php.



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
/**
 * An exception caused by a connectivity error
 * 
 * @copyright  Copyright (c) 2007-2008 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fConnectivityException
 * 
 * @version    1.0.0b
 * @changes    1.0.0b  The initial implementation [wb, 2007-06-14]
 */
class fConnectivityException extends fUnexpectedException
{
}



/**
 * Copyright (c) 2007-2008 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fCookie.php.

























































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
<?php
/**
 * Provides a consistent cookie API, HTTPOnly compatibility with older PHP versions and default parameters
 * 
 * @copyright  Copyright (c) 2008-2009 Will Bond, others
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @author     Nick Trew [nt]
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fCookie
 * 
 * @version    1.0.0b3
 * @changes    1.0.0b3  Added the ::delete() method [nt+wb, 2009-09-30]
 * @changes    1.0.0b2  Updated for new fCore API [wb, 2009-02-16]
 * @changes    1.0.0b   The initial implementation [wb, 2008-09-01]
 */
class fCookie
{
	// The following constants allow for nice looking callbacks to static methods
	const delete             = 'fCookie::delete';
	const get                = 'fCookie::get';
	const reset              = 'fCookie::reset';
	const set                = 'fCookie::set';
	const setDefaultDomain   = 'fCookie::setDefaultDomain';
	const setDefaultExpires  = 'fCookie::setDefaultExpires';
	const setDefaultHTTPOnly = 'fCookie::setDefaultHTTPOnly';
	const setDefaultPath     = 'fCookie::setDefaultPath';
	const setDefaultSecure   = 'fCookie::setDefaultSecure';
	
	
	/**
	 * The default domain to set for cookies
	 * 
	 * @var string
	 */
	static private $default_domain = NULL;
	
	/**
	 * The default expiration date to set for cookies
	 * 
	 * @var string|integer
	 */
	static private $default_expires = NULL;
	
	/**
	 * If cookies should default to being http-only
	 * 
	 * @var boolean
	 */
	static private $default_httponly = FALSE;
	
	/**
	 * The default path to set for cookies
	 * 
	 * @var string
	 */
	static private $default_path = NULL;
	
	/**
	 * If cookies should default to being secure-only
	 * 
	 * @var boolean
	 */
	static private $default_secure = FALSE;
	
	
	/**
	 * Deletes a cookie - uses default parameters set by the other set methods of this class
	 * 
	 * @param  string  $name    The cookie name to delete
	 * @param  string  $path    The path of the cookie to delete
	 * @param  string  $domain  The domain of the cookie to delete
	 * @param  boolean $secure  If the cookie is a secure-only cookie
	 * @return void
	 */
	static public function delete($name, $path=NULL, $domain=NULL, $secure=NULL)
	{
		self::set($name, '', time() - 86400, $path, $domain, $secure);
	}
	
	
	/**
	 * Gets a cookie value from `$_COOKIE`, while allowing a default value to be provided
	 * 
	 * @param  string $name           The name of the cookie to retrieve
	 * @param  mixed  $default_value  If there is no cookie with the name provided, return this value instead
	 * @return mixed  The value
	 */
	static public function get($name, $default_value=NULL)
	{
		if (isset($_COOKIE[$name])) {
			$value = fUTF8::clean($_COOKIE[$name]);
			if (get_magic_quotes_gpc()) {
				$value = stripslashes($value);
			}
			return $value;
		}
		return $default_value;
	}
	
	
	/**
	 * Resets the configuration of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		self::$default_domain   = NULL;
		self::$default_expires  = NULL;
		self::$default_httponly = FALSE;
		self::$default_path     = NULL;
		self::$default_secure   = FALSE;
	}
	
	
	/**
	 * Sets a cookie to be sent back to the browser - uses default parameters set by the other set methods of this class
	 * 
	 * The following methods allow for setting default parameters for this method:
	 *   
	 *  - ::setDefaultExpires():  Sets the default for the `$expires` parameter
	 *  - ::setDefaultPath():     Sets the default for the `$path` parameter
	 *  - ::setDefaultDomain():   Sets the default for the `$domain` parameter
	 *  - ::setDefaultSecure():   Sets the default for the `$secure` parameter
	 *  - ::setDefaultHTTPOnly(): Sets the default for the `$httponly` parameter
	 * 
	 * @param  string         $name      The name of the cookie to set
	 * @param  mixed          $value     The value of the cookie to set
	 * @param  string|integer $expires   A relative string to be interpreted by [http://php.net/strtotime strtotime()] or an integer unix timestamp
	 * @param  string         $path      The path this cookie applies to
	 * @param  string         $domain    The domain this cookie applies to
	 * @param  boolean        $secure    If the cookie should only be transmitted over a secure connection
	 * @param  boolean        $httponly  If the cookie should only be readable by HTTP connection, not javascript
	 * @return void
	 */
	static public function set($name, $value, $expires=NULL, $path=NULL, $domain=NULL, $secure=NULL, $httponly=NULL)
	{
		if ($expires === NULL && self::$default_expires !== NULL) {
			$expires = self::$default_expires;	
		}
		
		if ($path === NULL && self::$default_path !== NULL) {
			$path = self::$default_path;	
		}
		
		if ($domain === NULL && self::$default_domain !== NULL) {
			$domain = self::$default_domain;	
		}
		
		if ($secure === NULL && self::$default_secure !== NULL) {
			$secure = self::$default_secure;	
		}
		
		if ($httponly === NULL && self::$default_httponly !== NULL) {
			$httponly = self::$default_httponly;	
		}
		
		if ($expires && !is_numeric($expires)) {
			$expires = strtotime($expires);	
		}
		
		// Adds support for httponly cookies to PHP 5.0 and 5.1
		if (strlen($value) && $httponly && !fCore::checkVersion('5.2')) {
			$header_string = urlencode($name) . '=' . urlencode($value);
			if ($expires) {
				$header_string .= '; expires=' . gmdate('D, d-M-Y H:i:s T', $expires); 		
			}
			if ($path) {
				$header_string .= '; path=' . $path;	
			}
			if ($domain) {
				$header_string .= '; domain=' . $domain;	
			}
			if ($secure) {
				$header_string .= '; secure';	
			}
			$header_string .= '; httponly';
			header('Set-Cookie: ' . $header_string, FALSE);
			return;
			
		// Only pases the httponly parameter if we are on 5.2 since it causes error notices otherwise
		} elseif (strlen($value) && $httponly) {
			setcookie($name, $value, $expires, $path, $domain, $secure, TRUE);
			return; 		
		}
		
		setcookie($name, $value, $expires, $path, $domain, $secure);
	}
	
	
	/**
	 * Sets the default domain to use for cookies
	 * 
	 * This value will be used when the `$domain` parameter of the ::set()
	 * method is not specified or is set to `NULL`.
	 * 
	 * @param  string $domain  The default domain to use for cookies
	 * @return void
	 */
	static public function setDefaultDomain($domain)
	{
		self::$default_domain = $domain;	
	}
	
	
	/**
	 * Sets the default expiration date to use for cookies
	 * 
	 * This value will be used when the `$expires` parameter of the ::set()
	 * method is not specified or is set to `NULL`.
	 * 
	 * @param  string|integer $expires  The default expiration date to use for cookies
	 * @return void
	 */
	static public function setDefaultExpires($expires)
	{
		self::$default_expires = $expires;	
	}
	
	
	/**
	 * Sets the default httponly flag to use for cookies
	 * 
	 * This value will be used when the `$httponly` parameter of the ::set()
	 * method is not specified or is set to `NULL`.
	 * 
	 * @param  boolean $httponly  The default httponly flag to use for cookies
	 * @return void
	 */
	static public function setDefaultHTTPOnly($httponly)
	{
		self::$default_httponly = $httponly;	
	}
	
	
	/**
	 * Sets the default path to use for cookies
	 * 
	 * This value will be used when the `$path` parameter of the ::set()
	 * method is not specified or is set to `NULL`.
	 * 
	 * @param  string $path  The default path to use for cookies
	 * @return void
	 */
	static public function setDefaultPath($path)
	{
		self::$default_path = $path;	
	}
	
	
	/**
	 * Sets the default secure flag to use for cookies
	 * 
	 * This value will be used when the `$secure` parameter of the ::set()
	 * method is not specified or is set to `NULL`.
	 * 
	 * @param  boolean $secure  The default secure flag to use for cookies
	 * @return void
	 */
	static public function setDefaultSecure($secure)
	{
		self::$default_secure = $secure;	
	}
	
	
	/**
	 * Forces use as a static class
	 * 
	 * @return fCookie
	 */
	private function __construct() { }
}



/**
 * Copyright (c) 2008-2009 Will Bond <will@flourishlib.com>, others
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fCore.php.



















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
<?php
/**
 * Provides low-level debugging, error and exception functionality
 * 
 * @copyright  Copyright (c) 2007-2011 Will Bond, others
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @author     Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
 * @author     Nick Trew [nt]
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fCore
 * 
 * @version    1.0.0b24
 * @changes    1.0.0b24  Backwards Compatibility Break - moved ::detectOpcodeCache() to fLoader::hasOpcodeCache() [wb, 2011-08-26]
 * @changes    1.0.0b23  Backwards Compatibility Break - changed the email subject of error/exception emails to include relevant file info, instead of the timestamp, for better email message threading [wb, 2011-06-20]
 * @changes    1.0.0b22  Fixed a bug with dumping arrays containing integers [wb, 2011-05-26]
 * @changes    1.0.0b21  Changed ::startErrorCapture() to allow "stacking" it via multiple calls, fixed a couple of bugs with ::dump() mangling strings in the form `int(1)`, fixed mispelling of `occurred` [wb, 2011-05-09]
 * @changes    1.0.0b20  Backwards Compatibility Break - Updated ::expose() to not wrap the data in HTML when running via CLI, and instead just append a newline [wb, 2011-02-24]
 * @changes    1.0.0b19  Added detection of AIX to ::checkOS() [wb, 2011-01-19]
 * @changes    1.0.0b18  Updated ::expose() to be able to accept multiple parameters [wb, 2011-01-10]
 * @changes    1.0.0b17  Fixed a bug with ::backtrace() triggering notices when an argument is not UTF-8 [wb, 2010-08-17]
 * @changes    1.0.0b16  Added the `$types` and `$regex` parameters to ::startErrorCapture() and the `$regex` parameter to ::stopErrorCapture() [wb, 2010-08-09]
 * @changes    1.0.0b15  Added ::startErrorCapture() and ::stopErrorCapture() [wb, 2010-07-05]
 * @changes    1.0.0b14  Changed ::enableExceptionHandling() to only call fException::printMessage() when the destination is not `html` and no callback has been defined, added ::configureSMTP() to allow using fSMTP for error and exception emails [wb, 2010-06-04]
 * @changes    1.0.0b13  Added the `$backtrace` parameter to ::backtrace() [wb, 2010-03-05]
 * @changes    1.0.0b12  Added ::getDebug() to check for the global debugging flag, added more specific BSD checks to ::checkOS() [wb, 2010-03-02]
 * @changes    1.0.0b11  Added ::detectOpcodeCache() [nt+wb, 2009-10-06]
 * @changes    1.0.0b10  Fixed ::expose() to properly display when output includes non-UTF-8 binary data [wb, 2009-06-29]
 * @changes    1.0.0b9   Added ::disableContext() to remove context info for exception/error handling, tweaked output for exceptions/errors [wb, 2009-06-28]
 * @changes    1.0.0b8   ::enableErrorHandling() and ::enableExceptionHandling() now accept multiple email addresses, and a much wider range of emails [wb-imarc, 2009-06-01]
 * @changes    1.0.0b7   ::backtrace() now properly replaces document root with {doc_root} on Windows [wb, 2009-05-02]
 * @changes    1.0.0b6   Fixed a bug with getting the server name for error messages when running on the command line [wb, 2009-03-11]
 * @changes    1.0.0b5   Fixed a bug with checking the error/exception destination when a log file is specified [wb, 2009-03-07]
 * @changes    1.0.0b4   Backwards compatibility break - ::getOS() and ::getPHPVersion() removed, replaced with ::checkOS() and ::checkVersion() [wb, 2009-02-16]
 * @changes    1.0.0b3   ::handleError() now displays what kind of error occurred as the heading [wb, 2009-02-15]
 * @changes    1.0.0b2   Added ::registerDebugCallback() [wb, 2009-02-07]
 * @changes    1.0.0b    The initial implementation [wb, 2007-09-25]
 */
class fCore
{
	// The following constants allow for nice looking callbacks to static methods
	const backtrace               = 'fCore::backtrace';
	const call                    = 'fCore::call';
	const callback                = 'fCore::callback';
	const checkOS                 = 'fCore::checkOS';
	const checkVersion            = 'fCore::checkVersion';
	const configureSMTP           = 'fCore::configureSMTP';
	const debug                   = 'fCore::debug';
	const disableContext          = 'fCore::disableContext';
	const dump                    = 'fCore::dump';
	const enableDebugging         = 'fCore::enableDebugging';
	const enableDynamicConstants  = 'fCore::enableDynamicConstants';
	const enableErrorHandling     = 'fCore::enableErrorHandling';
	const enableExceptionHandling = 'fCore::enableExceptionHandling';
	const expose                  = 'fCore::expose';
	const getDebug                = 'fCore::getDebug';
	const handleError             = 'fCore::handleError';
	const handleException         = 'fCore::handleException';
	const registerDebugCallback   = 'fCore::registerDebugCallback';
	const reset                   = 'fCore::reset';
	const sendMessagesOnShutdown  = 'fCore::sendMessagesOnShutdown';
	const startErrorCapture       = 'fCore::startErrorCapture';
	const stopErrorCapture        = 'fCore::stopErrorCapture';
	

	/**
	 * The nesting level of error capturing
	 *
	 * @var integer
	 */
	static private $captured_error_level = 0;

	/**
	 * A stack of regex to match errors to capture, one string per level
	 * 
	 * @var array
	 */
	static private $captured_error_regex = array();
	
	/**
	 * A stack of the types of errors to capture, one integer per level
	 * 
	 * @var array
	 */
	static private $captured_error_types = array();
	
	/**
	 * A stack of arrays of errors that have been captured, one array per level
	 * 
	 * @var array
	 */
	static private $captured_errors = array();

	/**
	 * A stack of the previous error handler, one callback per level
	 * 
	 * @var array
	 */
	static private $captured_errors_previous_handler = array();
	
	/**
	 * If the context info has been shown
	 * 
	 * @var boolean
	 */
	static private $context_shown = FALSE;
	
	/**
	 * If global debugging is enabled
	 * 
	 * @var boolean
	 */
	static private $debug = NULL;
	
	/**
	 * A callback to pass debug messages to
	 * 
	 * @var callback
	 */
	static private $debug_callback = NULL;
	
	/**
	 * If dynamic constants should be created
	 * 
	 * @var boolean
	 */
	static private $dynamic_constants = FALSE;
	
	/**
	 * Error destination
	 * 
	 * @var string
	 */
	static private $error_destination = 'html';
	
	/**
	 * An array of errors to be send to the destination upon page completion
	 * 
	 * @var array
	 */
	static private $error_message_queue = array();
	
	/**
	 * Exception destination
	 * 
	 * @var string
	 */
	static private $exception_destination = 'html';
	
	/**
	 * Exception handler callback
	 * 
	 * @var mixed
	 */
	static private $exception_handler_callback = NULL;
	
	/**
	 * Exception handler callback parameters
	 * 
	 * @var array
	 */
	static private $exception_handler_parameters = array();
	
	/**
	 * The message generated by the uncaught exception
	 * 
	 * @var string
	 */
	static private $exception_message = NULL;
	
	/**
	 * If this class is handling errors
	 * 
	 * @var boolean
	 */
	static private $handles_errors = FALSE;
	
	/**
	 * If this class is handling exceptions
	 * 
	 * @var boolean
	 */
	static private $handles_exceptions = FALSE;

	/**
	 * If the context info should be shown with errors/exceptions
	 * 
	 * @var boolean
	 */
	static private $show_context = TRUE;

	/**
	 * An array of the most significant lines from error and exception backtraces
	 * 
	 * @var array
	 */
	static private $significant_error_lines = array();
	
	/**
	 * An SMTP connection for sending error and exception emails
	 * 
	 * @var fSMTP
	 */
	static private $smtp_connection = NULL;
	
	/**
	 * The email address to send error emails from
	 * 
	 * @var string
	 */
	static private $smtp_from_email = NULL;
	
	
	/**
	 * Creates a nicely formatted backtrace to the the point where this method is called
	 * 
	 * @param  integer $remove_lines  The number of trailing lines to remove from the backtrace
	 * @param  array   $backtrace     A backtrace from [http://php.net/backtrace `debug_backtrace()`] to format - this is not usually required or desired
	 * @return string  The formatted backtrace
	 */
	static public function backtrace($remove_lines=0, $backtrace=NULL)
	{
		if ($remove_lines !== NULL && !is_numeric($remove_lines)) {
			$remove_lines = 0;
		}
		
		settype($remove_lines, 'integer');
		
		$doc_root  = realpath($_SERVER['DOCUMENT_ROOT']);
		$doc_root .= (substr($doc_root, -1) != DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
		
		if ($backtrace === NULL) {
			$backtrace = debug_backtrace();
		}
		
		while ($remove_lines > 0) {
			array_shift($backtrace);
			$remove_lines--;
		}
		
		$backtrace = array_reverse($backtrace);
		
		$bt_string = '';
		$i = 0;
		foreach ($backtrace as $call) {
			if ($i) {
				$bt_string .= "\n";
			}
			if (isset($call['file'])) {
				$bt_string .= str_replace($doc_root, '{doc_root}' . DIRECTORY_SEPARATOR, $call['file']) . '(' . $call['line'] . '): ';
			} else {
				$bt_string .= '[internal function]: ';
			}
			if (isset($call['class'])) {
				$bt_string .= $call['class'] . $call['type'];
			}
			if (isset($call['class']) || isset($call['function'])) {
				$bt_string .= $call['function'] . '(';
					$j = 0;
					if (!isset($call['args'])) {
						$call['args'] = array();
					}
					foreach ($call['args'] as $arg) {
						if ($j) {
							$bt_string .= ', ';
						}
						if (is_bool($arg)) {
							$bt_string .= ($arg) ? 'true' : 'false';
						} elseif (is_null($arg)) {
							$bt_string .= 'NULL';
						} elseif (is_array($arg)) {
							$bt_string .= 'Array';
						} elseif (is_object($arg)) {
							$bt_string .= 'Object(' . get_class($arg) . ')';
						} elseif (is_string($arg)) {
							// Shorten the UTF-8 string if it is too long
							if (strlen(utf8_decode($arg)) > 18) {
								// If we can't match as unicode, try single byte
								if (!preg_match('#^(.{0,15})#us', $arg, $short_arg)) {
									preg_match('#^(.{0,15})#s', $arg, $short_arg);
								}
								$arg  = $short_arg[0] . '...';
							}
							$bt_string .= "'" . $arg . "'";
						} else {
							$bt_string .= (string) $arg;
						}
						$j++;
					}
				$bt_string .= ')';
			}
			$i++;
		}
		
		return $bt_string;
	}
	
	
	/**
	 * Performs a [http://php.net/call_user_func call_user_func()], while translating PHP 5.2 static callback syntax for PHP 5.1 and 5.0
	 * 
	 * Parameters can be passed either as a single array of parameters or as
	 * multiple parameters.
	 * 
	 * {{{
	 * #!php
	 * // Passing multiple parameters in a normal fashion
	 * fCore::call('Class::method', TRUE, 0, 'test');
	 * 
	 * // Passing multiple parameters in a parameters array
	 * fCore::call('Class::method', array(TRUE, 0, 'test'));
	 * }}}
	 * 
	 * To pass parameters by reference they must be assigned to an
	 * array by reference and the function/method being called must accept those
	 * parameters by reference. If either condition is not met, the parameter
	 * will be passed by value.
	 * 
	 * {{{
	 * #!php
	 * // Passing parameters by reference
	 * fCore::call('Class::method', array(&$var1, &$var2));
	 * }}}
	 * 
	 * @param  callback $callback    The function or method to call
	 * @param  array    $parameters  The parameters to pass to the function/method
	 * @return mixed  The return value of the called function/method
	 */
	static public function call($callback, $parameters=array())
	{
		// Fix PHP 5.0 and 5.1 static callback syntax
		if (is_string($callback) && strpos($callback, '::') !== FALSE) {
			$callback = explode('::', $callback);
		}
		
		$parameters = array_slice(func_get_args(), 1);
		if (sizeof($parameters) == 1 && is_array($parameters[0])) {
			$parameters = $parameters[0];
		}
		
		return call_user_func_array($callback, $parameters);
	}
	
	
	/**
	 * Translates a Class::method style static method callback to array style for compatibility with PHP 5.0 and 5.1 and built-in PHP functions
	 * 
	 * @param  callback $callback  The callback to translate
	 * @return array  The translated callback
	 */
	static public function callback($callback)
	{
		if (is_string($callback) && strpos($callback, '::') !== FALSE) {
			return explode('::', $callback);
		}
		
		return $callback;
	}
	
	
	/**
	 * Checks an error/exception destination to make sure it is valid
	 * 
	 * @param  string $destination  The destination for the exception. An email, file or the string `'html'`.
	 * @return string|boolean  `'email'`, `'file'`, `'html'` or `FALSE`
	 */
	static private function checkDestination($destination)
	{
		if ($destination == 'html') {
			return 'html';
		}
		
		if (preg_match('~^(?:                                                                         # Allow leading whitespace
						   (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")                     # An "atom" or a quoted string
						   (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*  # A . plus another "atom" or a quoted string, any number of times
						  )@(?:                                                                       # The @ symbol
						   (?:[a-z0-9\\-]+\.)+[a-z]{2,}|                                              # Domain name
						   (?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])    # (or) IP addresses
						  )
						  (?:\s*,\s*                                                                  # Any number of other emails separated by a comma with surrounding spaces
						   (?:
							(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")
							(?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*
						   )@(?:
							(?:[a-z0-9\\-]+\.)+[a-z]{2,}|
							(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])
						   )
						  )*$~xiD', $destination)) {
			return 'email';
		}
		
		$path_info     = pathinfo($destination);
		$dir_exists    = file_exists($path_info['dirname']);
		$dir_writable  = ($dir_exists) ? is_writable($path_info['dirname']) : FALSE;
		$file_exists   = file_exists($destination);
		$file_writable = ($file_exists) ? is_writable($destination) : FALSE;
		
		if (!$dir_exists || ($dir_exists && ((!$file_exists && !$dir_writable) || ($file_exists && !$file_writable)))) {
			return FALSE;
		}
			
		return 'file';
	}
	
	
	/**
	 * Returns is the current OS is one of the OSes passed as a parameter
	 * 
	 * Valid OS strings are:
	 *  - `'linux'`
	 *  - `'aix'`
	 *  - `'bsd'`
	 *  - `'freebsd'`
	 *  - `'netbsd'`
	 *  - `'openbsd'`
	 *  - `'osx'`
	 *  - `'solaris'`
	 *  - `'windows'`
	 * 
	 * @param  string $os  The operating system to check - see method description for valid OSes
	 * @param  string ...
	 * @return boolean  If the current OS is included in the list of OSes passed as parameters
	 */
	static public function checkOS($os)
	{
		$oses = func_get_args();
		
		$valid_oses = array('linux', 'aix', 'bsd', 'freebsd', 'openbsd', 'netbsd', 'osx', 'solaris', 'windows');
		
		if ($invalid_oses = array_diff($oses, $valid_oses)) {
			throw new fProgrammerException(
				'One or more of the OSes specified, %$1s, is invalid. Must be one of: %2$s.',
				join(' ', $invalid_oses),
				join(', ', $valid_oses)
			);
		}
		
		$uname = php_uname('s');
		
		if (stripos($uname, 'linux') !== FALSE) {
			return in_array('linux', $oses);
		
		} elseif (stripos($uname, 'aix') !== FALSE) {
			return in_array('aix', $oses);
		
		} elseif (stripos($uname, 'netbsd') !== FALSE) {
			return in_array('netbsd', $oses) || in_array('bsd', $oses);
		
		} elseif (stripos($uname, 'openbsd') !== FALSE) {
			return in_array('openbsd', $oses) || in_array('bsd', $oses);
		
		} elseif (stripos($uname, 'freebsd') !== FALSE) {
			return in_array('freebsd', $oses) || in_array('bsd', $oses);
		
		} elseif (stripos($uname, 'solaris') !== FALSE || stripos($uname, 'sunos') !== FALSE) {
			return in_array('solaris', $oses);
		
		} elseif (stripos($uname, 'windows') !== FALSE) {
			return in_array('windows', $oses);
		
		} elseif (stripos($uname, 'darwin') !== FALSE) {
			return in_array('osx', $oses);
		} 
		
		throw new fEnvironmentException('Unable to determine the current OS');
	}
	
	
	/**
	 * Checks to see if the running version of PHP is greater or equal to the version passed
	 * 
	 * @return boolean  If the running version of PHP is greater or equal to the version passed
	 */
	static public function checkVersion($version)
	{
		static $running_version = NULL;
		
		if ($running_version === NULL) {
			$running_version = preg_replace(
				'#^(\d+\.\d+\.\d+).*$#D',
				'\1',
				PHP_VERSION
			);
		}
		
		return version_compare($running_version, $version, '>=');
	}
	
	
	/**
	 * Composes text using fText if loaded
	 * 
	 * @param  string  $message    The message to compose
	 * @param  mixed   $component  A string or number to insert into the message
	 * @param  mixed   ...
	 * @return string  The composed and possible translated message
	 */
	static private function compose($message)
	{
		$args = array_slice(func_get_args(), 1);
		
		if (class_exists('fText', FALSE)) {
			return call_user_func_array(
				array('fText', 'compose'),
				array($message, $args)
			);
		} else {
			return vsprintf($message, $args);
		}
	}
	
	
	/**
	 * Sets an fSMTP object to be used for sending error and exception emails
	 * 
	 * @param  fSMTP  $smtp        The SMTP connection to send emails over
	 * @param  string $from_email  The email address to use in the `From:` header
	 * @return void
	 */
	static public function configureSMTP($smtp, $from_email)
	{
		self::$smtp_connection = $smtp;
		self::$smtp_from_email = $from_email;
	}
	
	
	/**
	 * Prints a debugging message if global or code-specific debugging is enabled
	 * 
	 * @param  string  $message  The debug message
	 * @param  boolean $force    If debugging should be forced even when global debugging is off
	 * @return void
	 */
	static public function debug($message, $force=FALSE)
	{
		if ($force || self::$debug) {
			if (self::$debug_callback) {
				call_user_func(self::$debug_callback, $message);
			} else {
				self::expose($message);
			}
		}
	}
	
	
	/**
	 * Creates a string representation of any variable using predefined strings for booleans, `NULL` and empty strings
	 * 
	 * The string output format of this method is very similar to the output of
	 * [http://php.net/print_r print_r()] except that the following values
	 * are represented as special strings:
	 *   
	 *  - `TRUE`: `'{true}'`
	 *  - `FALSE`: `'{false}'`
	 *  - `NULL`: `'{null}'`
	 *  - `''`: `'{empty_string}'`
	 * 
	 * @param  mixed $data  The value to dump
	 * @return string  The string representation of the value
	 */
	static public function dump($data)
	{
		if (is_bool($data)) {
			return ($data) ? '{true}' : '{false}';
		
		} elseif (is_null($data)) {
			return '{null}';
		
		} elseif ($data === '') {
			return '{empty_string}';
		
		} elseif (is_array($data) || is_object($data)) {
			
			ob_start();
			var_dump($data);
			$output = ob_get_contents();
			ob_end_clean();
			
			// Make the var dump more like a print_r
			$output = preg_replace('#=>\n(  )+(?=[a-zA-Z]|&)#m', ' => ', $output);
			$output = str_replace('string(0) ""', '{empty_string}', $output);
			$output = preg_replace('#=> (&)?NULL#', '=> \1{null}', $output);
			$output = preg_replace('#=> (&)?bool\((false|true)\)#', '=> \1{\2}', $output);
			$output = preg_replace('#(?<=^|\] => )(?:float|int)\((-?\d+(?:.\d+)?)\)#', '\1', $output);
			$output = preg_replace('#string\(\d+\) "#', '', $output);
			$output = preg_replace('#"(\n(  )*)(?=\[|\})#', '\1', $output);
			$output = preg_replace('#((?:  )+)\["(.*?)"\]#', '\1[\2]', $output);
			$output = preg_replace('#(?:&)?array\(\d+\) \{\n((?:  )*)((?:  )(?=\[)|(?=\}))#', "Array\n\\1(\n\\1\\2", $output);
			$output = preg_replace('/object\((\w+)\)#\d+ \(\d+\) {\n((?:  )*)((?:  )(?=\[)|(?=\}))/', "\\1 Object\n\\2(\n\\2\\3", $output);
			$output = preg_replace('#^((?:  )+)}(?=\n|$)#m', "\\1)\n", $output);
			$output = substr($output, 0, -2) . ')';
			
			// Fix indenting issues with the var dump output
			$output_lines = explode("\n", $output);
			$new_output = array();
			$stack = 0;
			foreach ($output_lines as $line) {
				if (preg_match('#^((?:  )*)([^ ])#', $line, $match)) {
					$spaces = strlen($match[1]);
					if ($spaces && $match[2] == '(') {
						$stack += 1;
					}
					$new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
					if ($spaces && $match[2] == ')') {
						$stack -= 1;
					}
				} else {
					$new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
				}
			}
			
			return join("\n", $new_output);
			
		} else {
			return (string) $data;
		}
	}
	
	
	/**
	 * Disables including the context information with exception and error messages
	 * 
	 * The context information includes the following superglobals:
	 * 
	 *  - `$_SERVER`
	 *  - `$_POST`
	 *  - `$_GET`
	 *  - `$_SESSION`
	 *  - `$_FILES`
	 *  - `$_COOKIE`
	 * 
	 * @return void
	 */
	static public function disableContext()
	{
		self::$show_context = FALSE;
	}
	
	
	/**
	 * Enables debug messages globally, i.e. they will be shown for any call to ::debug()
	 * 
	 * @param  boolean $flag  If debugging messages should be shown
	 * @return void
	 */
	static public function enableDebugging($flag)
	{
		self::$debug = (boolean) $flag;
	}
	
	
	/**
	 * Turns on a feature where undefined constants are automatically created with the string value equivalent to the name
	 * 
	 * This functionality only works if ::enableErrorHandling() has been
	 * called first. This functionality may have a very slight performance
	 * impact since a `E_STRICT` error message must be captured and then a
	 * call to [http://php.net/define define()] is made.
	 * 
	 * @return void
	 */
	static public function enableDynamicConstants()
	{
		if (!self::$handles_errors) {
			throw new fProgrammerException(
				'Dynamic constants can not be enabled unless error handling has been enabled via %s',
				__CLASS__ . '::enableErrorHandling()'
			);
		}
		self::$dynamic_constants = TRUE;
	}
	
	
	/**
	 * Turns on developer-friendly error handling that includes context information including a backtrace and superglobal dumps
	 * 
	 * All errors that match the current
	 * [http://php.net/error_reporting error_reporting()] level will be
	 * redirected to the destination and will include a full backtrace. In
	 * addition, dumps of the following superglobals will be made to aid in
	 * debugging:
	 * 
	 *  - `$_SERVER`
	 *  - `$_POST`
	 *  - `$_GET`
	 *  - `$_SESSION`
	 *  - `$_FILES`
	 *  - `$_COOKIE`
	 * 
	 * The superglobal dumps are only done once per page, however a backtrace
	 * in included for each error.
	 * 
	 * If an email address is specified for the destination, only one email
	 * will be sent per script execution. If both error and
	 * [enableExceptionHandling() exception handling] are set to the same
	 * email address, the email will contain both errors and exceptions.
	 * 
	 * @param  string $destination  The destination for the errors and context information - an email address, a file path or the string `'html'`
	 * @return void
	 */
	static public function enableErrorHandling($destination)
	{
		if (!self::checkDestination($destination)) {
			return;
		}
		self::$error_destination = $destination;
		self::$handles_errors    = TRUE;
		set_error_handler(self::callback(self::handleError));
	}
	
	
	/**
	 * Turns on developer-friendly uncaught exception handling that includes context information including a backtrace and superglobal dumps
	 * 
	 * Any uncaught exception will be redirected to the destination specified,
	 * and the page will execute the `$closing_code` callback before exiting.
	 * The destination will receive a message with the exception messaage, a
	 * full backtrace and dumps of the following superglobals to aid in
	 * debugging:
	 * 
	 *  - `$_SERVER`
	 *  - `$_POST`
	 *  - `$_GET`
	 *  - `$_SESSION`
	 *  - `$_FILES`
	 *  - `$_COOKIE`
	 * 
	 * The superglobal dumps are only done once per page, however a backtrace
	 * in included for each error.
	 * 
	 * If an email address is specified for the destination, only one email
	 * will be sent per script execution.
	 * 
	 * If an email address is specified for the destination, only one email
	 * will be sent per script execution. If both exception and
	 * [enableErrorHandling() error handling] are set to the same
	 * email address, the email will contain both exceptions and errors.
	 * 
	 * @param  string   $destination   The destination for the exception and context information - an email address, a file path or the string `'html'`
	 * @param  callback $closing_code  This callback will happen after the exception is handled and before page execution stops. Good for printing a footer. If no callback is provided and the exception extends fException, fException::printMessage() will be called.
	 * @param  array    $parameters    The parameters to send to `$closing_code`
	 * @return void
	 */
	static public function enableExceptionHandling($destination, $closing_code=NULL, $parameters=array())
	{
		if (!self::checkDestination($destination)) {
			return;
		}
		self::$handles_exceptions           = TRUE;
		self::$exception_destination        = $destination;
		self::$exception_handler_callback   = $closing_code;
		if (!is_object($parameters)) {
			settype($parameters, 'array');
		} else {
			$parameters = array($parameters);
		}
		self::$exception_handler_parameters = $parameters;
		set_exception_handler(self::callback(self::handleException));
	}
	
	
	/**
	 * Prints the ::dump() of a value
	 *
	 * The dump will be printed in a `<pre>` tag with the class `exposed` if
	 * PHP is running anywhere but via the command line (cli). If PHP is
	 * running via the cli, the data will be printed, followed by a single
	 * line break (`\n`).
	 * 
	 * If multiple parameters are passed, they are exposed as an array.
	 * 
	 * @param  mixed $data  The value to show
	 * @param  mixed ...
	 * @return void
	 */
	static public function expose($data)
	{
		$args = func_get_args();
		if (count($args) > 1) {
			$data = $args;
		}
		if (PHP_SAPI != 'cli') {
			echo '<pre class="exposed">' . htmlspecialchars((string) self::dump($data), ENT_QUOTES) . '</pre>';
		} else {
			echo self::dump($data) . "\n";
		}
	}
	
	
	/**
	 * Generates some information about the context of an error or exception
	 * 
	 * @return string  A string containing `$_SERVER`, `$_GET`, `$_POST`, `$_FILES`, `$_SESSION` and `$_COOKIE`
	 */
	static private function generateContext()
	{
		return self::compose('Context') . "\n-------" .
			"\n\n\$_SERVER: "  . self::dump($_SERVER) .
			"\n\n\$_POST: " . self::dump($_POST) .
			"\n\n\$_GET: " . self::dump($_GET) .
			"\n\n\$_FILES: "   . self::dump($_FILES) .
			"\n\n\$_SESSION: " . self::dump((isset($_SESSION)) ? $_SESSION : NULL) .
			"\n\n\$_COOKIE: " . self::dump($_COOKIE);
	}
	
	
	/**
	 * If debugging is enabled
	 * 
	 * @param  boolean $force  If debugging is forced
	 * @return boolean  If debugging is enabled
	 */
	static public function getDebug($force=FALSE)
	{
		return self::$debug || $force;
	}
	
	
	/**
	 * Handles an error, creating the necessary context information and sending it to the specified destination
	 * 
	 * @internal
	 * 
	 * @param  integer $error_number   The error type
	 * @param  string  $error_string   The message for the error
	 * @param  string  $error_file     The file the error occurred in
	 * @param  integer $error_line     The line the error occurred on
	 * @param  array   $error_context  A references to all variables in scope at the occurence of the error
	 * @return void
	 */
	static public function handleError($error_number, $error_string, $error_file=NULL, $error_line=NULL, $error_context=NULL)
	{
		if (self::$dynamic_constants && $error_number == E_NOTICE) {
			if (preg_match("#^Use of undefined constant (\w+) - assumed '\w+'\$#D", $error_string, $matches)) {
				define($matches[1], $matches[1]);
				return;
			}
		}
		
		$capturing   = (bool) self::$captured_error_level;
		$level_match = (bool) (error_reporting() & $error_number);
		
		if (!$capturing && !$level_match) {
			return;
		}
		
		$doc_root  = realpath($_SERVER['DOCUMENT_ROOT']);
		$doc_root .= (substr($doc_root, -1) != '/' && substr($doc_root, -1) != '\\') ? '/' : '';
		
		$backtrace = self::backtrace(1);
		
		// Remove the reference to handleError
		$backtrace = preg_replace('#: fCore::handleError\(.*?\)$#', '', $backtrace);
		
		$error_string = preg_replace('# \[<a href=\'.*?</a>\]: #', ': ', $error_string);
		
		// This was added in 5.2
		if (!defined('E_RECOVERABLE_ERROR')) {
			define('E_RECOVERABLE_ERROR', 4096);
		}
		
		// These were added in 5.3
		if (!defined('E_DEPRECATED')) {
			define('E_DEPRECATED', 8192);
		}
		
		if (!defined('E_USER_DEPRECATED')) {
			define('E_USER_DEPRECATED', 16384);
		}
		
		switch ($error_number) {
			case E_WARNING:           $type = self::compose('Warning');           break;
			case E_NOTICE:            $type = self::compose('Notice');            break;
			case E_USER_ERROR:        $type = self::compose('User Error');        break;
			case E_USER_WARNING:      $type = self::compose('User Warning');      break;
			case E_USER_NOTICE:       $type = self::compose('User Notice');       break;
			case E_STRICT:            $type = self::compose('Strict');            break;
			case E_RECOVERABLE_ERROR: $type = self::compose('Recoverable Error'); break;
			case E_DEPRECATED:        $type = self::compose('Deprecated');        break;
			case E_USER_DEPRECATED:   $type = self::compose('User Deprecated');   break;
		}
		
		if ($capturing) {
			$type_to_capture   = (bool) (self::$captured_error_types[self::$captured_error_level] & $error_number);
			$string_to_capture = !self::$captured_error_regex[self::$captured_error_level] || (self::$captured_error_regex[self::$captured_error_level] && preg_match(self::$captured_error_regex[self::$captured_error_level], $error_string));
			if ($type_to_capture && $string_to_capture) {
				self::$captured_errors[self::$captured_error_level][] = array(
					'number'    => $error_number,
					'type'      => $type,
					'string'    => $error_string,
					'file'      => str_replace($doc_root, '{doc_root}/', $error_file),
					'line'      => $error_line,
					'backtrace' => $backtrace,
					'context'   => $error_context
				);
				return;
			}
			
			// If the old handler is not this method, then we must have been trying to match a regex and failed
			// so we pass the error on to the original handler to do its thing
			if (self::$captured_errors_previous_handler[self::$captured_error_level] != array('fCore', 'handleError')) {
				if (self::$captured_errors_previous_handler[self::$captured_error_level] === NULL) {
					return FALSE;
				}
				return call_user_func(self::$captured_errors_previous_handler[self::$captured_error_level], $error_number, $error_string, $error_file, $error_line, $error_context);
			
			// If we get here, this method is the error handler, but we don't want to actually report the error so we return
			} elseif (!$level_match) {
				return;
			}
		}
		
		$error = $type . "\n" . str_pad('', strlen($type), '-') . "\n" . $backtrace . "\n" . $error_string;

		$backtrace_lines = explode("\n", $backtrace);
		
		self::sendMessageToDestination('error', $error, end($backtrace_lines));
	}
	
	
	/**
	 * Handles an uncaught exception, creating the necessary context information, sending it to the specified destination and finally executing the closing callback
	 * 
	 * @internal
	 * 
	 * @param  object $exception  The uncaught exception to handle
	 * @return void
	 */
	static public function handleException($exception)
	{
		$message = ($exception->getMessage()) ? $exception->getMessage() : '{no message}';
		if ($exception instanceof fException) {
			$trace = $exception->formatTrace();
		} else {
			$trace = $exception->getTraceAsString();
		}
		$code = ($exception->getCode()) ? ' (code ' . $exception->getCode() . ')' : '';
		
		$info       = $trace . "\n" . $message . $code;
		$headline   = self::compose("Uncaught") . " " . get_class($exception);
		$info_block = $headline . "\n" . str_pad('', strlen($headline), '-') . "\n" . trim($info);
		
		$trace_lines = explode("\n", $trace);

		self::sendMessageToDestination('exception', $info_block, end($trace_lines));
		
		if (self::$exception_handler_callback === NULL) {
			if (self::$exception_destination != 'html' && $exception instanceof fException) {
				$exception->printMessage();
			}
			return;
		}
				
		try {
			self::call(self::$exception_handler_callback, self::$exception_handler_parameters);
		} catch (Exception $e) {
			trigger_error(
				self::compose(
					'An exception was thrown in the %s closing code callback',
					'setExceptionHandling()'
				),
				E_USER_ERROR
			);
		}
	}
	
	
	/**
	 * Registers a callback to handle debug messages instead of the default action of calling ::expose() on the message
	 * 
	 * @param  callback $callback  A callback that accepts a single parameter, the string debug message to handle
	 * @return void
	 */
	static public function registerDebugCallback($callback)
	{
		self::$debug_callback = self::callback($callback);	
	}
	
	
	/**
	 * Resets the configuration of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		if (self::$handles_errors) {
			restore_error_handler();
		}
		if (self::$handles_exceptions) {
			restore_exception_handler();
		}
		
		if (is_array(self::$captured_errors)) {
			restore_error_handler();
		}
		
		self::$captured_error_level             = 0;
		self::$captured_error_regex             = array();
		self::$captured_error_types             = array();
		self::$captured_errors                  = array();
		self::$captured_errors_previous_handler = array();
		self::$context_shown                    = FALSE;
		self::$debug                            = NULL;
		self::$debug_callback                   = NULL;
		self::$dynamic_constants                = FALSE;
		self::$error_destination                = 'html';
		self::$error_message_queue              = array();
		self::$exception_destination            = 'html';
		self::$exception_handler_callback       = NULL;
		self::$exception_handler_parameters     = array();
		self::$exception_message                = NULL;
		self::$handles_errors                   = FALSE;
		self::$handles_exceptions               = FALSE;
		self::$significant_error_lines          = array();
		self::$show_context                     = TRUE;
		self::$smtp_connection                  = NULL;
		self::$smtp_from_email                  = NULL;
	}
	
	
	/**
	 * Sends an email or writes a file with messages generated during the page execution
	 * 
	 * This method prevents multiple emails from being sent or a log file from
	 * being written multiple times for one script execution.
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function sendMessagesOnShutdown()
	{		
		$messages = array();
		
		if (self::$error_message_queue) {
			$message = join("\n\n", self::$error_message_queue);
			$messages[self::$error_destination] = $message;
		}
		
		if (self::$exception_message) {
			if (isset($messages[self::$exception_destination])) {
				$messages[self::$exception_destination] .= "\n\n";
			} else {
				$messages[self::$exception_destination] = '';
			}
			$messages[self::$exception_destination] .= self::$exception_message;
		}

		$hash = md5(join('', self::$significant_error_lines), TRUE);
		$hash = strtr(base64_encode($hash), '/', '-');
		$hash = substr(rtrim($hash, '='), 0, 8);

		$first_file_line = preg_replace(
			'#^.*[/\\\\](.*)$#',
			'\1',
			reset(self::$significant_error_lines)
		);
		
		$subject = self::compose(
			'[%1$s] %2$s error(s) beginning at %3$s {%4$s}',
			isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : php_uname('n'),
			count($messages),
			$first_file_line,
			$hash
		);
		
		foreach ($messages as $destination => $message) {
			if (self::$show_context) {
				$message .= "\n\n" . self::generateContext();
			}
			
			if (self::checkDestination($destination) == 'email') {
				if (self::$smtp_connection) {
					$email = new fEmail();
					foreach (explode(',', $destination) as $recipient) {
						$email->addRecipient($recipient);
					}
					$email->setFromEmail(self::$smtp_from_email);
					$email->setSubject($subject);
					$email->setBody($message);
					$email->send(self::$smtp_connection);
				} else {
					mail($destination, $subject, $message);
				}
			
			} else {
				$handle = fopen($destination, 'a');
				fwrite($handle, $subject . "\n\n");
				fwrite($handle, $message . "\n\n");
				fclose($handle);
			}
		}
	}
	
	
	/**
	 * Handles sending a message to a destination
	 * 
	 * If the destination is an email address or file, the messages will be
	 * spooled up until the end of the script execution to prevent multiple
	 * emails from being sent or a log file being written to multiple times.
	 * 
	 * @param  string $type              If the message is an error or an exception
	 * @param  string $message           The message to send to the destination
	 * @param  string $significant_line  The most significant line from an error or exception backtrace
	 * @return void
	 */
	static private function sendMessageToDestination($type, $message, $significant_line)
	{
		$destination = ($type == 'exception') ? self::$exception_destination : self::$error_destination;
		
		if ($destination == 'html') {
			if (self::$show_context && !self::$context_shown) {
				self::expose(self::generateContext());
				self::$context_shown = TRUE;
			}
			self::expose($message);
			return;
		}

		static $registered_function = FALSE;
		if (!$registered_function) {
			register_shutdown_function(self::callback(self::sendMessagesOnShutdown));
			$registered_function = TRUE;
		}
		
		if ($type == 'error') {
			self::$error_message_queue[] = $message;
		} else {
			self::$exception_message = $message;
		}

		self::$significant_error_lines[] = $significant_line;
	}
	
	
	/**
	 * Temporarily enables capturing error messages 
	 * 
	 * @param  integer $types  The error types to capture - this should be as specific as possible - defaults to all (E_ALL | E_STRICT)
	 * @param  string  $regex  A PCRE regex to match against the error message
	 * @return void
	 */
	static public function startErrorCapture($types=NULL, $regex=NULL)
	{
		if ($types === NULL) {
			$types = E_ALL | E_STRICT;
		}

		self::$captured_error_level++;

		self::$captured_error_regex[self::$captured_error_level]             = $regex;
		self::$captured_error_types[self::$captured_error_level]             = $types;
		self::$captured_errors[self::$captured_error_level]                  = array();
		self::$captured_errors_previous_handler[self::$captured_error_level] = set_error_handler(self::callback(self::handleError));
	}
	
	
	/**
	 * Stops capturing error messages, returning all that have been captured
	 * 
	 * @param  string $regex  A PCRE regex to filter messages by
	 * @return array  The captured error messages
	 */
	static public function stopErrorCapture($regex=NULL)
	{
		$captures = self::$captured_errors[self::$captured_error_level];

		self::$captured_error_level--;

		self::$captured_error_regex             = array_slice(self::$captured_error_regex,             0, self::$captured_error_level, TRUE);
		self::$captured_error_types             = array_slice(self::$captured_error_types,             0, self::$captured_error_level, TRUE);
		self::$captured_errors                  = array_slice(self::$captured_errors,                  0, self::$captured_error_level, TRUE);
		self::$captured_errors_previous_handler = array_slice(self::$captured_errors_previous_handler, 0, self::$captured_error_level, TRUE);
		
		restore_error_handler();
		
		if ($regex) {
			$new_captures = array();
			foreach ($captures as $capture) {
				if (!preg_match($regex, $capture['string'])) { continue; }
				$new_captures[] = $capture;
			}
			$captures = $new_captures;
		}
		
		return $captures;
	}
	
	
	/**
	 * Forces use as a static class
	 * 
	 * @return fCore
	 */
	private function __construct() { }
}



/**
 * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fCryptography.php.









































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
<?php
/**
 * Provides cryptography functionality, including hashing, symmetric-key encryption and public-key encryption
 * 
 * @copyright  Copyright (c) 2007-2011 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fCryptography
 * 
 * @version    1.0.0b14
 * @changes    1.0.0b14  Added the `base36`, `base56` and custom types to ::randomString() [wb, 2011-08-25]
 * @changes    1.0.0b13  Updated documentation about symmetric-key encryption to explicitly state block and key sizes, added base64 type to ::randomString() [wb, 2010-11-06]
 * @changes    1.0.0b12  Fixed an inline comment that incorrectly references AES-256 [wb, 2010-11-04]
 * @changes    1.0.0b11  Updated class to use fCore::startErrorCapture() instead of `error_reporting()` [wb, 2010-08-09]
 * @changes    1.0.0b10  Added a missing parameter to an fProgrammerException in ::randomString() [wb, 2010-07-29]
 * @changes    1.0.0b9   Added ::hashHMAC() [wb, 2010-04-20]
 * @changes    1.0.0b8   Fixed ::seedRandom() to pass a directory instead of a file to [http://php.net/disk_free_space `disk_free_space()`] [wb, 2010-03-09]
 * @changes    1.0.0b7   SECURITY FIX: fixed issue with ::random() and ::randomString() not producing random output on OSX, made ::seedRandom() more robust [wb, 2009-10-06]
 * @changes    1.0.0b6   Changed ::symmetricKeyEncrypt() to throw an fValidationException when the $secret_key is less than 8 characters [wb, 2009-09-30]
 * @changes    1.0.0b5   Fixed a bug where some windows machines would throw an exception when generating random strings or numbers [wb, 2009-06-09]
 * @changes    1.0.0b4   Updated for new fCore API [wb, 2009-02-16]
 * @changes    1.0.0b3   Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
 * @changes    1.0.0b2   Backwards compatibility break - changed ::symmetricKeyEncrypt() to not encrypt the IV since we are using HMAC on it [wb, 2009-01-26]
 * @changes    1.0.0b    The initial implementation [wb, 2007-11-27]
 */
class fCryptography
{
	// The following constants allow for nice looking callbacks to static methods
	const checkPasswordHash   = 'fCryptography::checkPasswordHash';
	const hashHMAC            = 'fCryptography::hashHMAC';
	const hashPassword        = 'fCryptography::hashPassword';
	const publicKeyDecrypt    = 'fCryptography::publicKeyDecrypt';
	const publicKeyEncrypt    = 'fCryptography::publicKeyEncrypt';
	const publicKeySign       = 'fCryptography::publicKeySign';
	const publicKeyVerify     = 'fCryptography::publicKeyVerify';
	const random              = 'fCryptography::random';
	const randomString        = 'fCryptography::randomString';
	const symmetricKeyDecrypt = 'fCryptography::symmetricKeyDecrypt';
	const symmetricKeyEncrypt = 'fCryptography::symmetricKeyEncrypt';
	
	
	/**
	 * Checks a password against a hash created with ::hashPassword()
	 * 
	 * @param  string $password  The password to check
	 * @param  string $hash      The hash to check against
	 * @return boolean  If the password matches the hash
	 */
	static public function checkPasswordHash($password, $hash)
	{
		$salt = substr($hash, 29, 10);
		
		if (self::hashWithSalt($password, $salt) == $hash) {
			return TRUE;
		}
		
		return FALSE;
	}
	
	
	/**
	 * Create a private key resource based on a filename and password
	 * 
	 * @throws fValidationException  When the private key is invalid
	 * 
	 * @param  string $private_key_file  The path to a PEM-encoded private key
	 * @param  string $password          The password for the private key
	 * @return resource  The private key resource
	 */
	static private function createPrivateKeyResource($private_key_file, $password)
	{
		if (!file_exists($private_key_file)) {
			throw new fProgrammerException(
				'The path to the PEM-encoded private key specified, %s, is not valid',
				$private_key_file
			);
		}
		if (!is_readable($private_key_file)) {
			throw new fEnvironmentException(
				'The PEM-encoded private key specified, %s, is not readable',
				$private_key_file
			);
		}
		
		$private_key          = file_get_contents($private_key_file);
		$private_key_resource = openssl_pkey_get_private($private_key, $password);
		
		if ($private_key_resource === FALSE) {
			throw new fValidationException(
				'The private key file specified, %s, does not appear to be a valid private key or the password provided is incorrect',
				$private_key_file
			);
		}
		
		return $private_key_resource;
	}
	
	
	/**
	 * Create a public key resource based on a filename
	 * 
	 * @param  string $public_key_file  The path to an X.509 public key certificate
	 * @return resource  The public key resource
	 */
	static private function createPublicKeyResource($public_key_file)
	{
		if (!file_exists($public_key_file)) {
			throw new fProgrammerException(
				'The path to the X.509 certificate specified, %s, is not valid',
				$public_key_file
			);
		}
		if (!is_readable($public_key_file)) {
			throw new fEnvironmentException(
				'The X.509 certificate specified, %s, can not be read',
				$public_key_file
			);
		}
		
		$public_key = file_get_contents($public_key_file);
		$public_key_resource = openssl_pkey_get_public($public_key);
		
		if ($public_key_resource === FALSE) {
			throw new fProgrammerException(
				'The public key certificate specified, %s, does not appear to be a valid certificate',
				$public_key_file
			);
		}
		
		return $public_key_resource;
	}
	
	
	/**
	 * Provides a pure PHP implementation of `hash_hmac()` for when the hash extension is not installed
	 * 
	 * @internal
	 * 
	 * @param  string $algorithm  The hashing algorithm to use: `'md5'` or `'sha1'`
	 * @param  string $data       The data to create an HMAC for
	 * @param  string $key        The key to generate the HMAC with 
	 * @return string  The HMAC
	 */
	static public function hashHMAC($algorithm, $data, $key)
	{
		if (function_exists('hash_hmac')) {
			return hash_hmac($algorithm, $data, $key);
		}
		
		// Algorithm from http://www.ietf.org/rfc/rfc2104.txt
		if (strlen($key) > 64) {
			$key = pack('H*', $algorithm($key));
		}
		$key  = str_pad($key, 64, "\x0");
		
		$ipad = str_repeat("\x36", 64);
		$opad = str_repeat("\x5C", 64);
		
		return $algorithm(($key ^ $opad) . pack('H*', $algorithm(($key ^ $ipad) . $data)));
	}
	
	
	/**
	 * Hashes a password using a loop of sha1 hashes and a salt, making rainbow table attacks infeasible
	 * 
	 * @param  string $password  The password to hash
	 * @return string  An 80 character string of the Flourish fingerprint, salt and hashed password
	 */
	static public function hashPassword($password)
	{
		$salt = self::randomString(10);
		
		return self::hashWithSalt($password, $salt);
	}
	
	
	/**
	 * Performs a large iteration of hashing a string with a salt
	 * 
	 * @param  string $source  The string to hash
	 * @param  string $salt    The salt for the hash
	 * @return string  An 80 character string of the Flourish fingerprint, salt and hashed password
	 */
	static private function hashWithSalt($source, $salt)
	{
		$sha1 = sha1($salt . $source);
		for ($i = 0; $i < 1000; $i++) {
			$sha1 = sha1($sha1 . (($i % 2 == 0) ? $source : $salt));
		}
		
		return 'fCryptography::password_hash#' . $salt . '#' . $sha1;
	}
		
	
	/**
	 * Decrypts ciphertext encrypted using public-key encryption via ::publicKeyEncrypt()
	 * 
	 * A public key (X.509 certificate) is required for encryption and a
	 * private key (PEM) is required for decryption.
	 * 
	 * @throws fValidationException  When the ciphertext appears to be corrupted
	 * 
	 * @param  string $ciphertext        The content to be decrypted
	 * @param  string $private_key_file  The path to a PEM-encoded private key
	 * @param  string $password          The password for the private key
	 * @return string  The decrypted plaintext
	 */
	static public function publicKeyDecrypt($ciphertext, $private_key_file, $password)
	{
		self::verifyPublicKeyEnvironment();
		
		$private_key_resource = self::createPrivateKeyResource($private_key_file, $password);
		
		$elements = explode('#', $ciphertext);
		
		// We need to make sure this ciphertext came from here, otherwise we are gonna have issues decrypting it
		if (sizeof($elements) != 4 || $elements[0] != 'fCryptography::public') {
			throw new fProgrammerException(
				'The ciphertext provided does not appear to have been encrypted using %s',
				__CLASS__ . '::publicKeyEncrypt()'
			);
		}
		
		$encrypted_key = base64_decode($elements[1]);
		$ciphertext    = base64_decode($elements[2]);
		$provided_hmac = $elements[3];
		
		$plaintext = '';
		$result = openssl_open($ciphertext, $plaintext, $encrypted_key, $private_key_resource);
		openssl_free_key($private_key_resource);
		
		if ($result === FALSE) {
			throw new fEnvironmentException(
				'There was an unknown error decrypting the ciphertext provided'
			);
		}
		
		$hmac = self::hashHMAC('sha1', $encrypted_key . $ciphertext, $plaintext);
		
		// By verifying the HMAC we ensure the integrity of the data
		if ($hmac != $provided_hmac) {
			throw new fValidationException(
				'The ciphertext provided appears to have been tampered with or corrupted'
			);
		}
		
		return $plaintext;
	}
	
	
	/**
	 * Encrypts the passed data using public key encryption via OpenSSL
	 * 
	 * A public key (X.509 certificate) is required for encryption and a
	 * private key (PEM) is required for decryption.
	 * 
	 * @param  string $plaintext        The content to be encrypted
	 * @param  string $public_key_file  The path to an X.509 public key certificate
	 * @return string  A base-64 encoded result containing a Flourish fingerprint and suitable for decryption using ::publicKeyDecrypt()
	 */
	static public function publicKeyEncrypt($plaintext, $public_key_file)
	{
		self::verifyPublicKeyEnvironment();
		
		$public_key_resource = self::createPublicKeyResource($public_key_file);
		
		$ciphertext     = '';
		$encrypted_keys = array();
		$result = openssl_seal($plaintext, $ciphertext, $encrypted_keys, array($public_key_resource));
		openssl_free_key($public_key_resource);
		
		if ($result === FALSE) {
			throw new fEnvironmentException(
				'There was an unknown error encrypting the plaintext provided'
			);
		}
		
		$hmac = self::hashHMAC('sha1', $encrypted_keys[0] . $ciphertext, $plaintext);
		
		return 'fCryptography::public#' . base64_encode($encrypted_keys[0]) . '#' . base64_encode($ciphertext) . '#' . $hmac;
	}
	
	
	/**
	 * Creates a signature for plaintext to allow verification of the creator
	 * 
	 * A private key (PEM) is required for signing and a public key
	 * (X.509 certificate) is required for verification.
	 * 
	 * @throws fValidationException  When the private key is invalid
	 * 
	 * @param  string $plaintext         The content to be signed
	 * @param  string $private_key_file  The path to a PEM-encoded private key
	 * @param  string $password          The password for the private key
	 * @return string  The base64-encoded signature suitable for verification using ::publicKeyVerify()
	 */
	static public function publicKeySign($plaintext, $private_key_file, $password)
	{
		self::verifyPublicKeyEnvironment();
		
		$private_key_resource = self::createPrivateKeyResource($private_key_file, $password);
		
		$result = openssl_sign($plaintext, $signature, $private_key_resource);
		openssl_free_key($private_key_resource);
		
		if (!$result) {
			throw new fEnvironmentException(
				'There was an unknown error signing the plaintext'
			);
		}
		
		return base64_encode($signature);
	}
	
	
	/**
	 * Checks a signature for plaintext to verify the creator - works with ::publicKeySign()
	 * 
	 * A private key (PEM) is required for signing and a public key
	 * (X.509 certificate) is required for verification.
	 * 
	 * @param  string $plaintext         The content to check
	 * @param  string $signature         The base64-encoded signature for the plaintext
	 * @param  string $public_key_file   The path to an X.509 public key certificate
	 * @return boolean  If the public key file is the public key of the user who signed the plaintext
	 */
	static public function publicKeyVerify($plaintext, $signature, $public_key_file)
	{
		self::verifyPublicKeyEnvironment();
		
		$public_key_resource = self::createPublicKeyResource($public_key_file);
		
		$result = openssl_verify($plaintext, base64_decode($signature), $public_key_resource);
		openssl_free_key($public_key_resource);
		
		if ($result === -1) {
			throw new fEnvironmentException(
				'There was an unknown error verifying the plaintext and signature against the public key specified'
			);
		}
		
		return ($result === 1) ? TRUE : FALSE;
	}
	
	
	/**
	 * Generates a random number using [http://php.net/mt_rand mt_rand()] after ensuring a good PRNG seed
	 * 
	 * @param  integer $min  The minimum number to return
	 * @param  integer $max  The maximum number to return
	 * @return integer  The psuedo-random number
	 */
	static public function random($min=NULL, $max=NULL)
	{
		self::seedRandom();
		if ($min !== NULL || $max !== NULL) {
			return mt_rand($min, $max);
		}
		return mt_rand();
	}
	
	
	/**
	 * Returns a random string of the type and length specified
	 * 
	 * @param  integer $length  The length of string to return
	 * @param  string  $type    The type of string to return: `'base64'`, `'base56'`, `'base36'`, `'alphanumeric'`, `'alpha'`, `'numeric'`, or `'hexadecimal'`, if a different string is provided, it will be used for the alphabet
	 * @return string  A random string of the type and length specified
	 */
	static public function randomString($length, $type='alphanumeric')
	{
		if ($length < 1) {
			throw new fProgrammerException(
				'The length specified, %1$s, is less than the minimum of %2$s',
				$length,
				1
			);
		}
		
		switch ($type) {
			case 'base64':
				$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/';
				break;
				
			case 'alphanumeric':
				$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
				break;

			case 'base56':
				$alphabet = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789';
				break;
				
			case 'alpha':
				$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
				break;
			
			case 'base36':
				$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
				break;

			case 'hexadecimal':
				$alphabet = 'abcdef0123456789';
				break;
				
			case 'numeric':
				$alphabet = '0123456789';
				break;
				
			default:
				$alphabet = $type;
		}
		
		$alphabet_length = strlen($alphabet);
		$output = '';
		
		for ($i = 0; $i < $length; $i++) {
			$output .= $alphabet[self::random(0, $alphabet_length-1)];
		}
		
		return $output;
	}
	
	
	/**
	 * Makes sure that the PRNG has been seeded with a fairly secure value
	 * 
	 * @return void
	 */
	static private function seedRandom()
	{
		static $seeded = FALSE;
		
		if ($seeded) {
			return;
		}
		
		fCore::startErrorCapture(E_WARNING);
		
		$bytes = NULL;
		
		// On linux/unix/solaris we should be able to use /dev/urandom
		if (!fCore::checkOS('windows') && $handle = fopen('/dev/urandom', 'rb')) {
			$bytes = fread($handle, 4);
			fclose($handle);
				
		// On windows we should be able to use the Cryptographic Application Programming Interface COM object
		} elseif (fCore::checkOS('windows') && class_exists('COM', FALSE)) {
			try {
				// This COM object no longer seems to work on PHP 5.2.9+, no response on the bug report yet
				$capi  = new COM('CAPICOM.Utilities.1');
				$bytes = base64_decode($capi->getrandom(4, 0));
				unset($capi);
			} catch (Exception $e) { }
		}
		
		// If we could not use the OS random number generators we get some of the most unique info we can		
		if (!$bytes) {
			$string = microtime(TRUE) . uniqid('', TRUE) . join('', stat(__FILE__)) . disk_free_space(dirname(__FILE__));
			$bytes  = substr(pack('H*', md5($string)), 0, 4);
		}
		
		fCore::stopErrorCapture();
		
		$seed = (int) (base_convert(bin2hex($bytes), 16, 10) - 2147483647);
		
		mt_srand($seed);
		
		$seeded = TRUE;
	}
	
	
	/**
	 * Decrypts ciphertext encrypted using symmetric-key encryption via ::symmetricKeyEncrypt()
	 * 
	 * Since this is symmetric-key cryptography, the same key is used for
	 * encryption and decryption.
	 * 
	 * @throws fValidationException  When the ciphertext appears to be corrupted
	 * 
	 * @param  string $ciphertext  The content to be decrypted
	 * @param  string $secret_key  The secret key to use for decryption
	 * @return string  The decrypted plaintext
	 */
	static public function symmetricKeyDecrypt($ciphertext, $secret_key)
	{
		self::verifySymmetricKeyEnvironment();
		
		$elements = explode('#', $ciphertext);
		
		// We need to make sure this ciphertext came from here, otherwise we are gonna have issues decrypting it
		if (sizeof($elements) != 4 || $elements[0] != 'fCryptography::symmetric') {
			throw new fProgrammerException(
				'The ciphertext provided does not appear to have been encrypted using %s',
				__CLASS__ . '::symmetricKeyEncrypt()'
			);
		}
		
		$iv            = base64_decode($elements[1]);
		$ciphertext    = base64_decode($elements[2]);
		$provided_hmac = $elements[3];
		
		$hmac = self::hashHMAC('sha1', $iv . '#' . $ciphertext, $secret_key);
		
		// By verifying the HMAC we ensure the integrity of the data
		if ($hmac != $provided_hmac) {
			throw new fValidationException(
				'The ciphertext provided appears to have been tampered with or corrupted'
			);
		}
		
		// This code uses the Rijndael cipher with a 192 bit block size and a 256 bit key in cipher feedback mode
		$module   = mcrypt_module_open('rijndael-192', '', 'cfb', '');
		$key      = substr(sha1($secret_key), 0, mcrypt_enc_get_key_size($module));
		mcrypt_generic_init($module, $key, $iv);
		
		fCore::startErrorCapture(E_WARNING);
		$plaintext = mdecrypt_generic($module, $ciphertext);
		fCore::stopErrorCapture();
		
		mcrypt_generic_deinit($module);
		mcrypt_module_close($module);
		
		return $plaintext;
	}
	
	
	/**
	 * Encrypts the passed data using symmetric-key encryption
	 *
	 * Since this is symmetric-key cryptography, the same key is used for
	 * encryption and decryption.
	 * 
	 * @throws fValidationException  When the $secret_key is less than 8 characters long
	 *  
	 * @param  string $plaintext   The content to be encrypted
	 * @param  string $secret_key  The secret key to use for encryption - must be at least 8 characters
	 * @return string  An encrypted and base-64 encoded result containing a Flourish fingerprint and suitable for decryption using ::symmetricKeyDecrypt()
	 */
	static public function symmetricKeyEncrypt($plaintext, $secret_key)
	{
		if (strlen($secret_key) < 8) {
			throw new fValidationException(
				'The secret key specified does not meet the minimum requirement of being at least %s characters long',
				8
			);
		}
		
		self::verifySymmetricKeyEnvironment();
		
		// This code uses the Rijndael cipher with a 192 bit block size and a
		// 256 bit key in cipher feedback mode. Cipher feedback mode is chosen
		// because no extra padding is added, ensuring we always get the exact
		// same plaintext out of the decrypt method
		$module   = mcrypt_module_open('rijndael-192', '', 'cfb', '');
		$key      = substr(sha1($secret_key), 0, mcrypt_enc_get_key_size($module));
		srand();
		$iv       = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND);
		
		// Finish the main encryption
		mcrypt_generic_init($module, $key, $iv);
		
		fCore::startErrorCapture(E_WARNING);
		$ciphertext = mcrypt_generic($module, $plaintext);
		fCore::stopErrorCapture();
		
		// Clean up the main encryption
		mcrypt_generic_deinit($module);
		mcrypt_module_close($module);
		
		// Here we are generating the HMAC for the encrypted data to ensure data integrity
		$hmac = self::hashHMAC('sha1', $iv . '#' . $ciphertext, $secret_key);
		
		// All of the data is then encoded using base64 to prevent issues with character sets
		$encoded_iv         = base64_encode($iv);
		$encoded_ciphertext = base64_encode($ciphertext);
		
		// Indicate in the resulting encrypted data what the encryption tool was
		return 'fCryptography::symmetric#' . $encoded_iv . '#' . $encoded_ciphertext . '#' . $hmac;
	}
	
	
	/**
	 * Makes sure the required PHP extensions and library versions are all correct
	 * 
	 * @return void
	 */
	static private function verifyPublicKeyEnvironment()
	{
		if (!extension_loaded('openssl')) {
			throw new fEnvironmentException(
				'The PHP %s extension is required, however is does not appear to be loaded',
				'openssl'
			);
		}
	}
	
	
	/**
	 * Makes sure the required PHP extensions and library versions are all correct
	 * 
	 * @return void
	 */
	static private function verifySymmetricKeyEnvironment()
	{
		if (!extension_loaded('mcrypt')) {
			throw new fEnvironmentException(
				'The PHP %s extension is required, however is does not appear to be loaded',
				'mcrypt'
			);
		}
		if (!function_exists('mcrypt_module_open')) {
			throw new fEnvironmentException(
				'The cipher used, %1$s (also known as %2$s), requires libmcrypt version 2.4.x or newer. The version installed does not appear to meet this requirement.',
				'AES-192',
				'rijndael-192'
			);
		}
		if (!in_array('rijndael-192', mcrypt_list_algorithms())) {
			throw new fEnvironmentException(
				'The cipher used, %1$s (also known as %2$s), does not appear to be supported by the installed version of libmcrypt',
				'AES-192',
				'rijndael-192'
			);
		}
	}
	
	
	/**
	 * Forces use as a static class
	 * 
	 * @return fSecurity
	 */
	private function __construct() { }
}



/**
 * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fDatabase.php.

















































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
3085
3086
3087
3088
3089
3090
3091
3092
3093
3094
3095
3096
3097
3098
3099
3100
3101
3102
3103
3104
3105
3106
3107
3108
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
3147
3148
3149
3150
3151
3152
3153
3154
3155
3156
3157
3158
3159
3160
3161
3162
3163
3164
3165
3166
3167
3168
3169
3170
3171
3172
3173
3174
3175
3176
3177
3178
3179
3180
3181
3182
3183
3184
3185
3186
3187
3188
3189
3190
3191
3192
3193
3194
3195
3196
3197
3198
3199
3200
3201
3202
3203
3204
3205
3206
3207
3208
3209
3210
3211
3212
3213
3214
3215
3216
3217
3218
3219
3220
3221
3222
3223
3224
3225
3226
3227
3228
3229
3230
3231
3232
3233
3234
3235
3236
3237
3238
3239
3240
3241
3242
3243
3244
3245
3246
3247
3248
3249
3250
3251
3252
3253
3254
3255
3256
3257
3258
3259
3260
3261
3262
3263
3264
3265
3266
3267
3268
3269
3270
3271
3272
3273
3274
3275
3276
3277
3278
3279
3280
3281
3282
3283
3284
3285
3286
3287
3288
3289
3290
3291
3292
3293
3294
3295
3296
3297
3298
3299
3300
3301
3302
3303
3304
3305
3306
3307
3308
3309
3310
3311
3312
3313
3314
3315
3316
3317
3318
3319
3320
3321
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
3365
3366
3367
3368
3369
3370
3371
3372
3373
3374
3375
3376
3377
3378
3379
3380
3381
3382
3383
3384
3385
3386
3387
3388
3389
3390
3391
3392
3393
3394
3395
3396
3397
3398
3399
3400
3401
3402
3403
3404
3405
3406
3407
3408
3409
3410
3411
3412
3413
3414
3415
3416
3417
3418
3419
3420
3421
3422
3423
3424
3425
3426
3427
3428
3429
3430
3431
3432
3433
3434
3435
3436
3437
3438
3439
3440
3441
3442
3443
3444
3445
3446
3447
3448
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
3490
3491
3492
3493
3494
3495
3496
3497
3498
3499
3500
3501
3502
3503
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
3534
3535
3536
3537
3538
3539
3540
3541
3542
3543
3544
3545
3546
3547
3548
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
3570
3571
3572
3573
3574
3575
3576
3577
3578
3579
3580
3581
3582
3583
3584
3585
3586
3587
3588
3589
3590
3591
3592
3593
3594
3595
3596
3597
3598
3599
3600
3601
3602
3603
3604
3605
3606
3607
3608
3609
3610
3611
3612
3613
3614
3615
3616
3617
3618
3619
3620
3621
3622
3623
3624
<?php
/**
 * Provides a common API for different databases - will automatically use any installed extension
 * 
 * This class is implemented to use the UTF-8 character encoding. Please see
 * http://flourishlib.com/docs/UTF-8 for more information.
 * 
 * The following databases are supported:
 * 
 *  - [http://ibm.com/db2 DB2]
 *  - [http://microsoft.com/sql/ MSSQL]
 *  - [http://mysql.com MySQL]
 *  - [http://oracle.com Oracle]
 *  - [http://postgresql.org PostgreSQL]
 *  - [http://sqlite.org SQLite]
 * 
 * The class will automatically use the first of the following extensions it finds:
 * 
 *  - DB2
 *   - [http://php.net/ibm_db2 ibm_db2]
 *   - [http://php.net/pdo_ibm pdo_ibm]
 *  - MSSQL
 *   - [http://msdn.microsoft.com/en-us/library/cc296221.aspx sqlsrv]
 *   - [http://php.net/pdo_dblib pdo_dblib]
 *   - [http://php.net/mssql mssql] (or [http://php.net/sybase sybase])
 *  - MySQL
 *   - [http://php.net/mysql mysql]
 *   - [http://php.net/mysqli mysqli]
 *   - [http://php.net/pdo_mysql pdo_mysql]
 *  - Oracle
 *   - [http://php.net/oci8 oci8]
 *   - [http://php.net/pdo_oci pdo_oci]
 *  - PostgreSQL
 *   - [http://php.net/pgsql pgsql]
 *   - [http://php.net/pdo_pgsql pdo_pgsql]
 *  - SQLite
 *   - [http://php.net/pdo_sqlite pdo_sqlite] (for v3.x)
 *   - [http://php.net/sqlite sqlite] (for v2.x)
 * 
 * The `odbc` and `pdo_odbc` extensions are not supported due to character
 * encoding and stability issues on Windows, and functionality on non-Windows
 * operating systems.
 * 
 * @copyright  Copyright (c) 2007-2011 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fDatabase
 * 
 * @version    1.0.0b40
 * @changes    1.0.0b40  Fixed a bug with notices being triggered when failing to connect to a SQLite database [wb, 2011-06-20]
 * @changes    1.0.0b39  Fixed a bug with detecting some MySQL database version numbers [wb, 2011-05-24]
 * @changes    1.0.0b38  Backwards Compatibility Break - callbacks registered to the `extracted` hook via ::registerHookCallback() no longer receive the `$strings` parameter, instead all strings are added into the `$values` parameter - added ::getVersion(), fixed a bug with SQLite messaging, fixed a bug with ::__destruct(), improved handling of transactional queries, added ::close(), enhanced class to throw four different exceptions for different connection errors, silenced PHP warnings upon connection error [wb, 2011-05-09]
 * @changes    1.0.0b37  Fixed usage of the mysqli extension to only call mysqli_set_charset() if it exists [wb, 2011-03-04]
 * @changes    1.0.0b36  Updated ::escape() and methods that use ::escape() to handle float values that don't contain a digit before or after the . [wb, 2011-02-01]
 * @changes    1.0.0b35  Updated the class to replace `LIMIT` and `OFFSET` value placeholders in the SQL with their values before translating since most databases that translate `LIMIT` statements need to move or add values together [wb, 2011-01-11]
 * @changes    1.0.0b34  Fixed a bug with creating translated prepared statements [wb, 2011-01-09]
 * @changes    1.0.0b33  Added code to explicitly set the connection encoding for the mysql and mysqli extensions since some PHP installs don't see to fully respect `SET NAMES` [wb, 2010-12-06]
 * @changes    1.0.0b32  Fixed handling auto-incrementing values for Oracle when the trigger was on `INSERT OR UPDATE` instead of just `INSERT` [wb, 2010-12-04]
 * @changes    1.0.0b31  Fixed handling auto-incrementing values for MySQL when the `INTO` keyword is left out of an `INSERT` statement [wb, 2010-11-04]
 * @changes    1.0.0b30  Fixed the pgsql, mssql and mysql extensions to force a new connection instead of reusing an existing one [wb, 2010-08-17]
 * @changes    1.0.0b29  Backwards Compatibility Break - removed ::enableSlowQueryWarnings(), added ability to replicate via ::registerHookCallback() [wb, 2010-08-10]
 * @changes    1.0.0b28  Backwards Compatibility Break - removed ODBC support. Added support for the `pdo_ibm` extension. [wb, 2010-07-31]
 * @changes    1.0.0b27  Fixed a bug with running multiple copies of a SQL statement with string values through a single ::translatedQuery() call [wb, 2010-07-14]
 * @changes    1.0.0b26  Updated the class to use new fCore functionality [wb, 2010-07-05]
 * @changes    1.0.0b25  Added IBM DB2 support [wb, 2010-04-13]
 * @changes    1.0.0b24  Fixed an auto-incrementing transaction bug with Oracle and debugging issues with all databases [wb, 2010-03-17]
 * @changes    1.0.0b23  Resolved another bug with capturing auto-incrementing values for PostgreSQL and Oracle [wb, 2010-03-15]
 * @changes    1.0.0b22  Changed ::clearCache() to also clear the cache on the fSQLTranslation [wb, 2010-03-09]
 * @changes    1.0.0b21  Added ::execute() for result-less SQL queries, ::prepare() and ::translatedPrepare() to create fStatement objects for prepared statements, support for prepared statements in ::query() and ::unbufferedQuery(), fixed default caching key for ::enableCaching() [wb, 2010-03-02]
 * @changes    1.0.0b20  Added a parameter to ::enableCaching() to provide a key token that will allow cached values to be shared between multiple databases with the same schema [wb, 2009-10-28]
 * @changes    1.0.0b19  Added support for escaping identifiers (column and table names) to ::escape(), added support for database schemas, rewrote internal SQL string spliting [wb, 2009-10-22]
 * @changes    1.0.0b18  Updated the class for the new fResult and fUnbufferedResult APIs, fixed ::unescape() to not touch NULLs [wb, 2009-08-12]
 * @changes    1.0.0b17  Added the ability to pass an array of all values as a single parameter to ::escape() instead of one value per parameter [wb, 2009-08-11]
 * @changes    1.0.0b16  Fixed PostgreSQL and Oracle from trying to get auto-incrementing values on inserts when explicit values were given [wb, 2009-08-06]
 * @changes    1.0.0b15  Fixed a bug where auto-incremented values would not be detected when table names were quoted [wb, 2009-07-15]
 * @changes    1.0.0b14  Changed ::determineExtension() and ::determineCharacterSet() to be protected instead of private [wb, 2009-07-08]
 * @changes    1.0.0b13  Updated ::escape() to accept arrays of values for insertion into full SQL strings [wb, 2009-07-06]
 * @changes    1.0.0b12  Updates to ::unescape() to improve performance [wb, 2009-06-15]
 * @changes    1.0.0b11  Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
 * @changes    1.0.0b10  Changed date/time/timestamp escaping from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
 * @changes    1.0.0b9   Fixed a bug with ::escape() where floats that start with a . were encoded as `NULL` [wb, 2009-05-09]
 * @changes    1.0.0b8   Added Oracle support, change PostgreSQL code to no longer cause lastval() warnings, added support for arrays of values to ::escape() [wb, 2009-05-03]
 * @changes    1.0.0b7   Updated for new fCore API [wb, 2009-02-16]
 * @changes    1.0.0b6   Fixed a bug with executing transaction queries when using the mysqli extension [wb, 2009-02-12]
 * @changes    1.0.0b5   Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
 * @changes    1.0.0b4   Added a few error suppression operators back in so that developers don't get errors and exceptions [wb, 2009-01-14]
 * @changes    1.0.0b3   Removed some unnecessary error suppresion operators [wb, 2008-12-11]
 * @changes    1.0.0b2   Fixed a bug with PostgreSQL when using the PDO extension and executing an INSERT statement [wb, 2008-12-11]
 * @changes    1.0.0b    The initial implementation [wb, 2007-09-25]
 */
class fDatabase
{
	/**
	 * Composes text using fText if loaded
	 * 
	 * @param  string  $message    The message to compose
	 * @param  mixed   $component  A string or number to insert into the message
	 * @param  mixed   ...
	 * @return string  The composed and possible translated message
	 */
	static protected function compose($message)
	{
		$args = array_slice(func_get_args(), 1);
		
		if (class_exists('fText', FALSE)) {
			return call_user_func_array(
				array('fText', 'compose'),
				array($message, $args)
			);
		} else {
			return vsprintf($message, $args);
		}
	}
	
	
	/**
	 * An fCache object to cache the schema info to
	 * 
	 * @var fCache
	 */
	private $cache;
	
	/**
	 * The cache prefix to use for cache entries
	 * 
	 * @var string
	 */
	private $cache_prefix;
	
	/**
	 * Database connection resource or PDO object
	 * 
	 * @var mixed
	 */
	private $connection;
	
	/**
	 * The database name
	 * 
	 * @var string
	 */
	private $database;
	
	/**
	 * If debugging is enabled
	 * 
	 * @var boolean
	 */
	private $debug;
	
	/**
	 * A temporary error holder for the mssql extension
	 * 
	 * @var string
	 */
	private $error;
	
	/**
	 * The extension to use for the database specified
	 * 
	 * Options include:
	 * 
	 *  - `'ibm_db2'`
	 *  - `'mssql'`
	 *  - `'mysql'`
	 *  - `'mysqli'`
	 *  - `'oci8'`
	 *  - `'pgsql'`
	 *  - `'sqlite'`
	 *  - `'sqlsrv'`
	 *  - `'pdo'`
	 * 
	 * @var string
	 */
	protected $extension;
	
	/**
	 * Hooks callbacks to be used for accessing and modifying queries
	 * 
	 * This array will have the structure:
	 * 
	 * {{{
	 * array(
	 *     'unmodified' => array({callbacks}),
	 *     'extracted'  => array({callbacks}),
	 *     'run'        => array({callbacks})
	 * )
	 * }}}
	 * 
	 * @var array
	 */
	private $hook_callbacks;
	
	/**
	 * The host the database server is located on
	 * 
	 * @var string
	 */
	private $host;
	
	/**
	 * If a transaction is in progress
	 * 
	 * @var boolean
	 */
	private $inside_transaction;
	
	/**
	 * The password for the user specified
	 * 
	 * @var string
	 */
	private $password;
	
	/**
	 * The port number for the host
	 * 
	 * @var string
	 */
	private $port;
	
	/**
	 * The total number of seconds spent executing queries
	 * 
	 * @var float
	 */
	private $query_time;
	
	/**
	 * A cache of database-specific code
	 * 
	 * @var array 
	 */
	protected $schema_info;
	
	/**
	 * The last executed fStatement object
	 * 
	 * @var fStatement
	 */
	private $statement;

	/**
	 * The timeout for the database connection
	 *
	 * @var integer
	 */
	private $timeout;
	
	/**
	 * The fSQLTranslation object for this database
	 * 
	 * @var object
	 */
	private $translation;
	
	/**
	 * The database type: `'db2'`, `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, or `'sqlite'`
	 * 
	 * @var string
	 */
	private $type;
	
	/**
	 * The unbuffered query instance
	 * 
	 * @var fUnbufferedResult
	 */
	private $unbuffered_result;
	
	/**
	 * The user to connect to the database as
	 * 
	 * @var string
	 */
	private $username;
	
	
	/**
	 * Configures the connection to a database - connection is not made until the first query is executed
	 *
	 * Passing `NULL` to any parameter other than `$type` and `$database` will
	 * cause the default value to be used.
	 * 
	 * @param  string  $type      The type of the database: `'db2'`, `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'`
	 * @param  string  $database  Name of the database. If SQLite the path to the database file.
	 * @param  string  $username  Database username - not used for SQLite
	 * @param  string  $password  The password for the username specified - not used for SQLite
	 * @param  string  $host      Database server host or IP, defaults to localhost - not used for SQLite. MySQL socket connection can be made by entering `'sock:'` followed by the socket path. PostgreSQL socket connection can be made by passing just `'sock:'`. 
	 * @param  integer $port      The port to connect to, defaults to the standard port for the database type specified - not used for SQLite
	 * @param  integer $timeout   The number of seconds to timeout after if a connection can not be made - not used for SQLite
	 * @return fDatabase
	 */
	public function __construct($type, $database, $username=NULL, $password=NULL, $host=NULL, $port=NULL, $timeout=NULL)
	{
		$valid_types = array('db2', 'mssql', 'mysql', 'oracle', 'postgresql', 'sqlite');
		if (!in_array($type, $valid_types)) {
			throw new fProgrammerException(
				'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
				$type,
				join(', ', $valid_types)
			);
		}
		
		if (empty($database)) {
			throw new fProgrammerException('No database was specified');
		}
		
		if ($host === NULL) {
			$host = 'localhost';
		}
		
		$this->type     = $type;
		$this->database = $database;
		$this->username = $username;
		$this->password = $password;
		$this->host     = $host;
		$this->port     = $port;
		$this->timeout  = $timeout;
		
		$this->hook_callbacks = array(
			'unmodified' => array(),
			'extracted'  => array(),
			'run'        => array()
		);
		
		$this->schema_info = array();
		
		$this->determineExtension();
	}
	
	
	/**
	 * Closes the open database connection
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	public function __destruct()
	{
		if (!$this->connection) { return; }
		
		fCore::debug('Total query time: ' . $this->query_time . ' seconds', $this->debug);
		if ($this->extension == 'ibm_db2') {
			db2_close($this->connection);
		} elseif ($this->extension == 'mssql') {
			mssql_close($this->connection);
		} elseif ($this->extension == 'mysql') {
			mysql_close($this->connection);
		} elseif ($this->extension == 'mysqli') {
			// Before 5.2.0 the destructor order would cause mysqli to
			// close itself which would make this call trigger a warning
			if (fCore::checkVersion('5.2.0')) {
				mysqli_close($this->connection);
			}
		} elseif ($this->extension == 'oci8') {
			oci_close($this->connection);
		} elseif ($this->extension == 'pgsql') {
			pg_close($this->connection);
		} elseif ($this->extension == 'sqlite') {
			sqlite_close($this->connection);
		} elseif ($this->extension == 'sqlsrv') {
			sqlsrv_close($this->connection);
		} elseif ($this->extension == 'pdo') {
			// PDO objects close their own connections when destroyed
		}

		$this->connection = FALSE;
	}
	
	
	/**
	 * All requests that hit this method should be requests for callbacks
	 * 
	 * @internal
	 * 
	 * @param  string $method  The method to create a callback for
	 * @return callback  The callback for the method requested
	 */
	public function __get($method)
	{
		return array($this, $method);		
	}
	
	
	/**
	 * Checks to see if an SQL error occured
	 * 
	 * @param  fResult|fUnbufferedResult|boolean $result      The result object for the query
	 * @param  mixed                             $extra_info  The sqlite extension will pass a string error message, the oci8 extension will pass the statement resource
	 * @param  string                            $sql         The SQL that was executed
	 * @return void
	 */
	private function checkForError($result, $extra_info=NULL, $sql=NULL)
	{
		if ($result === FALSE || $result->getResult() === FALSE) {
			
			if ($this->extension == 'ibm_db2') {
				if (is_resource($extra_info)) {
					$message = db2_stmt_errormsg($extra_info);
				} else {
					$message = db2_stmt_errormsg();
				}
			} elseif ($this->extension == 'mssql') {
				$message = $this->error;
				$this->error = '';

			} elseif ($this->extension == 'mysql') {
				$message = mysql_error($this->connection);
			} elseif ($this->extension == 'mysqli') {
				if (is_object($extra_info)) {
					$message = $extra_info->error;	
				} else {
					$message = mysqli_error($this->connection);
				}
			} elseif ($this->extension == 'oci8') {
				$error_info = oci_error($extra_info ? $extra_info : $this->connection);
				$message = $error_info['message'];
			} elseif ($this->extension == 'pgsql') {
				$message = pg_last_error($this->connection);
			} elseif ($this->extension == 'sqlite') {
				if ($extra_info === NULL) {
					$message = sqlite_error_string(sqlite_last_error($this->connection));
				} else {
					$message = $extra_info;
				}
			} elseif ($this->extension == 'sqlsrv') {
				$error_info = sqlsrv_errors(SQLSRV_ERR_ALL);
				$message = $error_info[0]['message'];
			} elseif ($this->extension == 'pdo') {
				if ($extra_info instanceof PDOStatement) {
					$error_info = $extra_info->errorInfo();
				} else {
					$error_info = $this->connection->errorInfo();
				}
				
				if (empty($error_info[2])) {
					$error_info[2] = 'Unknown error - this usually indicates a bug in the PDO driver';	
				}
				$message = $error_info[2];
			}
			
			$db_type_map = array(
				'db2'        => 'DB2',
				'mssql'      => 'MSSQL',
				'mysql'      => 'MySQL',
				'oracle'     => 'Oracle',
				'postgresql' => 'PostgreSQL',
				'sqlite'     => 'SQLite'
			);
			
			throw new fSQLException(
				'%1$s error (%2$s) in %3$s',
				$db_type_map[$this->type],
				$message,
				is_object($result) ? $result->getSQL() : $sql
			);
		}
	}
	
	
	/**
	 * Clears all of the schema info out of the object and, if set, the fCache object
	 * 
	 * @return void
	 */
	public function clearCache()
	{
		$this->schema_info = array();
		if ($this->cache) {
			$this->cache->delete($this->makeCachePrefix() . 'schema_info');
		}
		if ($this->type == 'mssql') {
			$this->determineCharacterSet();		
		}
		if ($this->translation) {
			$this->translation->clearCache();	
		}
	}


	/**
	 * Closes the database connection
	 *
	 * @return void
	 */
	public function close()
	{
		$this->__destruct();
	}
	
	
	/**
	 * Connects to the database specified, if no connection exists
	 *
	 * This method is only intended to force a connection, all operations that
	 * require a database connection will automatically call this method.
	 * 
	 * @throws fAuthorizationException  When the username and password are not accepted
	 *
	 * @return void
	 */
	public function connect()
	{
		// Don't try to reconnect if we are already connected
		if ($this->connection) { return; }

		$connection_error     = FALSE;
		$authentication_error = FALSE;
		$database_error       = FALSE;

		$errors = NULL;

		// Establish a connection to the database
		if ($this->extension == 'pdo') {
			$username = $this->username;
			$password = $this->password;
			$options  = array();
			if ($this->timeout !== NULL && $this->type != 'sqlite' && $this->type != 'mssql') {
				$options[PDO::ATTR_TIMEOUT] = $this->timeout;
			}

			if ($this->type == 'db2') {
				if ($this->host === NULL && $this->port === NULL) {
					$dsn = 'ibm:DSN:' . $this->database;
				} else {
					$dsn  = 'ibm:DRIVER={IBM DB2 ODBC DRIVER};DATABASE=' . $this->database . ';HOSTNAME=' . $this->host . ';';
					$dsn .= 'PORT=' . ($this->port ? $this->port : 60000) . ';';
					$dsn .= 'PROTOCOL=TCPIP;UID=' . $username . ';PWD=' . $password . ';';
					if ($this->timeout !== NULL) {
						$dsn .= 'CONNECTTIMEOUT=' . $this->timeout . ';';
					}
					$username = NULL;
					$password = NULL;
				}
				
			} elseif ($this->type == 'mssql') {
				$separator = (fCore::checkOS('windows')) ? ',' : ':';
				$port      = ($this->port) ? $separator . $this->port : '';
				$driver    = (fCore::checkOs('windows')) ? 'mssql' : 'dblib';
				$dsn       = $driver . ':host=' . $this->host . $port . ';dbname=' . $this->database;
				
				// This driver does not support timeouts so we fake it here
				if ($this->timeout !== NULL) {
					fCore::startErrorCapture();
					$resource = fsockopen($this->host, $this->port ? $this->port : 1433, $errno, $errstr, $this->timeout);
					$errors = fCore::stopErrorCapture();
					if ($resource !== FALSE) {
						fclose($resource);
					}
				}
				
			} elseif ($this->type == 'mysql') {
				if (substr($this->host, 0, 5) == 'sock:') {
					$dsn = 'mysql:unix_socket=' . substr($this->host, 5) . ';dbname=' . $this->database;	
				} else {
					$port = ($this->port) ? ';port=' . $this->port : '';
					$dsn  = 'mysql:host=' . $this->host . ';dbname=' . $this->database . $port;
				}
				
			} elseif ($this->type == 'oracle') {
				$port = ($this->port) ? ':' . $this->port : '';
				$dsn  = 'oci:dbname=' . $this->host . $port . '/' . $this->database . ';charset=AL32UTF8';

				// This driver does not support timeouts so we fake it here
				if ($this->timeout !== NULL) {
					fCore::startErrorCapture();
					$resource = fsockopen($this->host, $this->port ? $this->port : 1521, $errno, $errstr, $this->timeout);
					$errors = fCore::stopErrorCapture();
					if ($resource !== FALSE) {
						fclose($resource);
					}
				}
				
			} elseif ($this->type == 'postgresql') {
				
				$dsn = 'pgsql:dbname=' . $this->database;
				if ($this->host && $this->host != 'sock:') {
					$dsn .= ' host=' . $this->host;	
				}
				if ($this->port) {
					$dsn .= ' port=' . $this->port;	
				}
				
			} elseif ($this->type == 'sqlite') {
				$dsn = 'sqlite:' . $this->database;
			}
			
			try {
				if ($errors) {
					$this->connection = FALSE;
				} else {
					$this->connection = new PDO($dsn, $username, $password, $options);	
					if ($this->type == 'mysql') {
						$this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1);	
					}
				}

			} catch (PDOException $e) {
				$this->connection = FALSE;

				$errors = $e->getMessage();
			}
		}
		
		if ($this->extension == 'sqlite') {
			$this->connection = sqlite_open($this->database);
		}
		
		if ($this->extension == 'ibm_db2') {
			$username = $this->username;
			$password = $this->password;
			if ($this->host === NULL && $this->port === NULL && $this->timeout === NULL) {
				$connection_string = $this->database;
			} else {
				$connection_string  = 'DATABASE=' . $this->database . ';HOSTNAME=' . $this->host . ';';
				$connection_string .= 'PORT=' . ($this->port ? $this->port : 60000) . ';';
				$connection_string .= 'PROTOCOL=TCPIP;UID=' . $this->username . ';PWD=' . $this->password . ';';
				if ($this->timeout !== NULL) {
					$connection_string .= 'CONNECTTIMEOUT=' . $this->timeout . ';';
				}
				$username = NULL;
				$password = NULL;
			}
			$options = array(
				'autocommit'    => DB2_AUTOCOMMIT_ON,
				'DB2_ATTR_CASE' => DB2_CASE_LOWER
			);
			$this->connection = db2_connect($connection_string, $username, $password, $options);
			if ($this->connection === FALSE) {
				$errors = db2_conn_errormsg();
			}
		}
		
		if ($this->extension == 'mssql') {
			if ($this->timeout !== NULL) {
				$old_timeout = ini_get('mssql.connect_timeout');
				ini_set('mssql.connect_timeout', $this->timeout);
			}

			fCore::startErrorCapture();
			
			$separator        = (fCore::checkOS('windows')) ? ',' : ':';
			$this->connection = mssql_connect(($this->port) ? $this->host . $separator . $this->port : $this->host, $this->username, $this->password, TRUE);

			if ($this->connection !== FALSE && mssql_select_db($this->database, $this->connection) === FALSE) {
				$this->connection = FALSE;
			}

			$errors = fCore::stopErrorCapture();

			if ($this->timeout !== NULL) {
				ini_set('mssql.connect_timeout', $old_timeout);
			}
		}
		
		if ($this->extension == 'mysql') {
			if ($this->timeout !== NULL) {
				$old_timeout = ini_get('mysql.connect_timeout');
				ini_set('mysql.connect_timeout', $this->timeout);
			}

			if (substr($this->host, 0, 5) == 'sock:') {
				$host = substr($this->host, 4);
			} elseif ($this->port) {
				$host = $this->host . ':' . $this->port;	
			} else {
				$host = $this->host;	
			}
			
			fCore::startErrorCapture();
			
			$this->connection = mysql_connect($host, $this->username, $this->password, TRUE);

			$errors = fCore::stopErrorCapture();

			if ($this->connection !== FALSE && mysql_select_db($this->database, $this->connection) === FALSE) {
				$errors = 'Unknown database';
				$this->connection = FALSE;
			}

			if ($this->connection && function_exists('mysql_set_charset') && !mysql_set_charset('utf8', $this->connection)) {
                throw new fConnectivityException(
                	'There was an error setting the database connection to use UTF-8'
				);
            }

            if ($this->timeout !== NULL) {
				ini_set('mysql.connect_timeout', $old_timeout);
			}
		}
			
		if ($this->extension == 'mysqli') {
			$this->connection = mysqli_init();
			if ($this->timeout !== NULL) {
				mysqli_options($this->connection, MYSQLI_OPT_CONNECT_TIMEOUT, $this->timeout);
			}

			fCore::startErrorCapture();

			if (substr($this->host, 0, 5) == 'sock:') {
				$result = mysqli_real_connect($this->connection, 'localhost', $this->username, $this->password, $this->database, $this->port, substr($this->host, 5));
			} elseif ($this->port) {
				$result = mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->database, $this->port);
			} else {
				$result = mysqli_real_connect($this->connection, $this->host, $this->username, $this->password, $this->database);
			}
			if (!$result) {
				$this->connection = FALSE;
			}

			$errors = fCore::stopErrorCapture();
			
			if ($this->connection && function_exists('mysqli_set_charset') && !mysqli_set_charset($this->connection, 'utf8')) {
                throw new fConnectivityException(
                	'There was an error setting the database connection to use UTF-8'
                );
            }
		}
		
		if ($this->extension == 'oci8') {

			fCore::startErrorCapture();
			$resource = TRUE;

			// This driver does not support timeouts so we fake it here
			if ($this->timeout !== NULL) {
				$resource = fsockopen($this->host, $this->port ? $this->port : 1521, $errno, $errstr, $this->timeout);
				if ($resource !== FALSE) {
					fclose($resource);
					$resource = TRUE;
				} else {
					$this->connection = FALSE;
				}
			}

			if ($resource) {
				$this->connection = oci_connect($this->username, $this->password, $this->host . ($this->port ? ':' . $this->port : '') . '/' . $this->database, 'AL32UTF8');
			}

			$errors = fCore::stopErrorCapture();
		}
			
		if ($this->extension == 'pgsql') {
			$connection_string = "dbname='" . addslashes($this->database) . "'";
			if ($this->host && $this->host != 'sock:') {
				$connection_string .= " host='" . addslashes($this->host) . "'";	
			}
			if ($this->username) {
				$connection_string .= " user='" . addslashes($this->username) . "'";
			}
			if ($this->password) {
				$connection_string .= " password='" . addslashes($this->password) . "'";
			}
			if ($this->port) {
				$connection_string .= " port='" . $this->port . "'";
			}
			if ($this->timeout !== NULL) {
				$connection_string .= " connect_timeout='" . $this->timeout . "'";
			}

			fCore::startErrorCapture();

			$this->connection = pg_connect($connection_string, PGSQL_CONNECT_FORCE_NEW);

			$errors = fCore::stopErrorCapture();
		}
		
		if ($this->extension == 'sqlsrv') {
			$options = array(
				'Database' => $this->database
			);
			if ($this->username !== NULL) {
				$options['UID'] = $this->username;
			}
			if ($this->password !== NULL) {
				$options['PWD'] = $this->password;
			}
			if ($this->timeout !== NULL) {
				$options['LoginTimeout'] = $this->timeout;
			}

			$this->connection = sqlsrv_connect($this->host . ',' . $this->port, $options);

			if ($this->connection === FALSE) {
				$errors = sqlsrv_errors();
			}

			sqlsrv_configure('WarningsReturnAsErrors', 0);
		}
		
		// Ensure the connection was established
		if ($this->connection === FALSE) {
			$this->handleConnectionErrors($errors);
		}
		
		// Make MySQL act more strict and use UTF-8
		if ($this->type == 'mysql') {
			$this->execute("SET SQL_MODE = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE'");
			$this->execute("SET NAMES 'utf8'");
			$this->execute("SET CHARACTER SET utf8");
		}
		
		// Make SQLite behave like other DBs for assoc arrays
		if ($this->type == 'sqlite') {
			$this->execute('PRAGMA short_column_names = 1');
		}
		
		// Fix some issues with mssql
		if ($this->type == 'mssql') {
			if (!isset($this->schema_info['character_set'])) {
				$this->determineCharacterSet();
			}
			$this->execute('SET TEXTSIZE 65536');
			$this->execute('SET QUOTED_IDENTIFIER ON');
		}
		
		// Make PostgreSQL use UTF-8
		if ($this->type == 'postgresql') {
			$this->execute("SET NAMES 'UTF8'");
		}
		
		// Oracle has different date and timestamp defaults
		if ($this->type == 'oracle') {
			$this->execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
			$this->execute("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
			$this->execute("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZR'");
			$this->execute("ALTER SESSION SET NLS_TIME_FORMAT = 'HH24:MI:SS'");
			$this->execute("ALTER SESSION SET NLS_TIME_TZ_FORMAT = 'HH24:MI:SS TZR'");
		}
	}
	
	
	/**
	 * Determines the character set of a SQL Server database
	 * 
	 * @return void
	 */
	protected function determineCharacterSet()
	{
		$this->schema_info['character_set'] = 'WINDOWS-1252';
		$this->schema_info['character_set'] = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(DB_NAME(), 'Collation')), 'CodePage')) AS charset")->fetchScalar();
		if ($this->cache) {
			$this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);	
		}
	}
	
	
	/**
	 * Figures out which extension to use for the database type selected
	 * 
	 * @return void
	 */
	protected function determineExtension()
	{
		switch ($this->type) {
			
			case 'db2':
				
				if (extension_loaded('ibm_db2')) {
					$this->extension = 'ibm_db2';
					
				} elseif (class_exists('PDO', FALSE) && in_array('ibm', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} else {
					$type = 'DB2';
					$exts = 'ibm_db2, pdo_ibm';
				}
				break;
			
			case 'mssql':
			
				if (extension_loaded('sqlsrv')) {
					$this->extension = 'sqlsrv';
					
				} elseif (extension_loaded('mssql')) {
					$this->extension = 'mssql';
					
				} elseif (class_exists('PDO', FALSE) && (in_array('dblib', PDO::getAvailableDrivers()) || in_array('mssql', PDO::getAvailableDrivers()))) {
					$this->extension = 'pdo';
					
				} else {
					$type = 'MSSQL';
					$exts = 'mssql, sqlsrv, pdo_dblib (linux), pdo_mssql (windows)';
				}
				break;
			
			
			case 'mysql':
			
				if (extension_loaded('mysqli')) {
					$this->extension = 'mysqli';
					
				} elseif (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} elseif (extension_loaded('mysql')) {
					$this->extension = 'mysql';
					
				} else {
					$type = 'MySQL';
					$exts = 'mysql, pdo_mysql, mysqli';
				}
				break;
				
				
			case 'oracle':
				
				if (extension_loaded('oci8')) {
					$this->extension = 'oci8';
					
				} elseif (class_exists('PDO', FALSE) && in_array('oci', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} else {
					$type = 'Oracle';
					$exts = 'oci8, pdo_oci';
				}
				break;
			
			
			case 'postgresql':
			
				if (extension_loaded('pgsql')) {
					$this->extension = 'pgsql';
					
				} elseif (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} else {
					$type = 'PostgreSQL';
					$exts = 'pgsql, pdo_pgsql';
				}
				break;
				
				
			case 'sqlite':
			
				$sqlite_version = 0;
				
				if (file_exists($this->database)) {
					
					$database_handle  = fopen($this->database, 'r');
					$database_version = fread($database_handle, 64);
					fclose($database_handle);
					
					if (strpos($database_version, 'SQLite format 3') !== FALSE) {
						$sqlite_version = 3;
					} elseif (strpos($database_version, '** This file contains an SQLite 2.1 database **') !== FALSE) {
						$sqlite_version = 2;
					} else {
						throw new fConnectivityException(
							'The database specified does not appear to be a valid %1$s or %2$s database',
							'SQLite v2.1',
							'v3'
						);
					}
				}
				
				if ((!$sqlite_version || $sqlite_version == 3) && class_exists('PDO', FALSE) && in_array('sqlite', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} elseif ($sqlite_version == 3 && (!class_exists('PDO', FALSE) || !in_array('sqlite', PDO::getAvailableDrivers()))) {
					throw new fEnvironmentException(
						'The database specified is an %1$s database and the %2$s extension is not installed',
						'SQLite v3',
						'pdo_sqlite'
					);
				
				} elseif ((!$sqlite_version || $sqlite_version == 2) && extension_loaded('sqlite')) {
					$this->extension = 'sqlite';
					
				} elseif ($sqlite_version == 2 && !extension_loaded('sqlite')) {
					throw new fEnvironmentException(
						'The database specified is an %1$s database and the %2$s extension is not installed',
						'SQLite v2.1',
						'sqlite'
					);
				
				} else {
					$type = 'SQLite';
					$exts = 'pdo_sqlite, sqlite';
				}
				break;
		}
		
		if (!$this->extension) {
			throw new fEnvironmentException(
				'The server does not have any of the following extensions for %2$s support: %2$s',
				$type,
				$exts
			);
		}
	}
	
	
	/**
	 * Sets the schema info to be cached to the fCache object specified
	 * 
	 * @param  fCache $cache      The cache to cache to
	 * @param  string $key_token  Internal use only! (this will be used in the cache key to uniquely identify the cache for this fDatabase object) 
	 * @return void
	 */
	public function enableCaching($cache, $key_token=NULL)
	{
		$this->cache = $cache;
		
		if ($key_token !== NULL) {
			$this->cache_prefix = 'fDatabase::' . $this->type . '::' . $key_token . '::';	
		}
		
		$this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array());
	}
	
	
	/**
	 * Sets if debug messages should be shown
	 * 
	 * @param  boolean $flag  If debugging messages should be shown
	 * @return void
	 */
	public function enableDebugging($flag)
	{
		$this->debug = (boolean) $flag;
	}
	
	
	/**
	 * Escapes a value for insertion into SQL
	 * 
	 * The valid data types are:
	 * 
	 *  - `'blob'`
	 *  - `'boolean'`
	 *  - `'date'`
	 *  - `'float'`
	 *  - `'identifier'`
	 *  - `'integer'`
	 *  - `'string'` (also varchar, char or text)
	 *  - `'varchar'`
	 *  - `'char'`
	 *  - `'text'`
	 *  - `'time'`
	 *  - `'timestamp'`
	 * 
	 * In addition to being able to specify the data type, you can also pass
	 * in an SQL statement with data type placeholders in the following form:
	 *   
	 *  - `%l` for a blob
	 *  - `%b` for a boolean
	 *  - `%d` for a date
	 *  - `%f` for a float
	 *  - `%r` for an indentifier (table or column name)
	 *  - `%i` for an integer
	 *  - `%s` for a string
	 *  - `%t` for a time
	 *  - `%p` for a timestamp
	 * 
	 * Depending on what `$sql_or_type` and `$value` are, the output will be
	 * slightly different. If `$sql_or_type` is a data type or a single
	 * placeholder and `$value` is:
	 * 
	 *  - a scalar value - an escaped SQL string is returned
	 *  - an array - an array of escaped SQL strings is returned
	 * 
	 * If `$sql_or_type` is a SQL string and `$value` is:
	 * 
	 *  - a scalar value - the escaped value is inserted into the SQL string
	 *  - an array - the escaped values are inserted into the SQL string separated by commas
	 * 
	 * If `$sql_or_type` is a SQL string, it is also possible to pass an array
	 * of all values as a single parameter instead of one value per parameter.
	 * An example would look like the following:
	 * 
	 * {{{
	 * #!php
	 * $db->escape(
	 *     "SELECT * FROM users WHERE status = %s AND authorization_level = %s",
	 *     array('Active', 'Admin')
	 * );
	 * }}}
	 * 
	 * @param  string $sql_or_type  This can either be the data type to escape or an SQL string with a data type placeholder - see method description
	 * @param  mixed  $value        The value to escape - both single values and arrays of values are supported, see method description for details
	 * @param  mixed  ...
	 * @return mixed  The escaped value/SQL or an array of the escaped values
	 */
	public function escape($sql_or_type, $value)
	{
		$values = array_slice(func_get_args(), 1);
		
		if (sizeof($values) < 1) {
			throw new fProgrammerException(
				'No value was specified to escape'
			);	
		}
		
		// Convert all objects into strings
		$values = $this->scalarize($values);
		$value  = array_shift($values);
		
		// Handle single value escaping
		$callback = NULL;
		
		switch ($sql_or_type) {
			case 'blob':
			case '%l':
				$callback = $this->escapeBlob;
				break;
			case 'boolean':
			case '%b':
				$callback = $this->escapeBoolean;
				break;
			case 'date':
			case '%d':
				$callback = $this->escapeDate;
				break;
			case 'float':
			case '%f':
				$callback = $this->escapeFloat;
				break;
			case 'identifier':
			case '%r':
				$callback = $this->escapeIdentifier;
				break;
			case 'integer':
			case '%i':
				$callback = $this->escapeInteger;
				break;
			case 'string':
			case 'varchar':
			case 'char':
			case 'text':
			case '%s':
				$callback = $this->escapeString;
				break;
			case 'time':
			case '%t':
				$callback = $this->escapeTime;
				break;
			case 'timestamp':
			case '%p':
				$callback = $this->escapeTimestamp;
				break;
		}
		
		if ($callback) {
			if (is_array($value)) {
				// If the values were passed as a single array, this handles that
				if (count($value) == 1 && is_array(current($value))) {
					$value = current($value);
				}
				return array_map($callback, $value);		
			}
			return call_user_func($callback, $value);
		}	
		
		// Separate the SQL from quoted values
		$parts = $this->splitSQL($sql_or_type, $placeholders);

		// If the values were passed as a single array, this handles that
		if (count($values) == 0 && is_array($value) && count($value) == $placeholders) {
			$values = $value;
			$value  = array_shift($values);	
		}

		array_unshift($values, $value);
		$sql = $this->extractStrings($parts, $values);
		return $this->escapeSQL($sql, $values, FALSE);
	}
	
	
	/**
	 * Escapes a blob for use in SQL, includes surround quotes when appropriate
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The blob to escape
	 * @return string  The escaped blob
	 */
	private function escapeBlob($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		$this->connect();
		
		if ($this->type == 'db2') {
			return "BLOB(X'" . bin2hex($value) . "')";
			
		} elseif ($this->type == 'mysql') {
			return "x'" . bin2hex($value) . "'";
			
		} elseif ($this->type == 'postgresql') {
			$output = '';
			for ($i=0; $i<strlen($value); $i++) {
				$output .= '\\\\' . str_pad(decoct(ord($value[$i])), 3, '0', STR_PAD_LEFT);
			}
			return "E'" . $output . "'";
			
		} elseif ($this->extension == 'sqlite') {
			return "'" . bin2hex($value) . "'";
			
		} elseif ($this->type == 'sqlite') {
			return "X'" . bin2hex($value) . "'";
			
		} elseif ($this->type == 'mssql') {
			return '0x' . bin2hex($value);
			
		} elseif ($this->type == 'oracle') {
			return "'" . bin2hex($value) . "'";
		}
	}
	
	
	/**
	 * Escapes a boolean for use in SQL, includes surround quotes when appropriate
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  boolean $value  The boolean to escape
	 * @return string  The database equivalent of the boolean passed
	 */
	private function escapeBoolean($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		if (in_array($this->type, array('postgresql', 'mysql'))) {
			return ($value) ? 'TRUE' : 'FALSE';
		} elseif (in_array($this->type, array('mssql', 'sqlite', 'db2'))) {
			return ($value) ? "'1'" : "'0'";
		} elseif ($this->type == 'oracle') {
			return ($value) ? '1' : '0';	
		}
	}
	
	
	/**
	 * Escapes a date for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The date to escape
	 * @return string  The escaped date
	 */
	private function escapeDate($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		try {
			$value = new fDate($value);
			return "'" . $value->format('Y-m-d') . "'";
			
		} catch (fValidationException $e) {
			return 'NULL';
		}
	}
	
	
	/**
	 * Escapes a float for use in SQL
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  float $value  The float to escape
	 * @return string  The escaped float
	 */
	private function escapeFloat($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		if (!strlen($value)) {
			return 'NULL';
		}
		if (!preg_match('#^[+\-]?([0-9]+(\.([0-9]+)?)?|(\.[0-9]+))$#D', $value)) {
			return 'NULL';
		}
		
		$value = rtrim($value, '.');
		$value = preg_replace('#(?<![0-9])\.#', '0.', $value);
		
		return (string) $value;
	}
	
	
	/**
	 * Escapes an identifier for use in SQL, necessary for reserved words
	 * 
	 * @param  string $value  The identifier to escape
	 * @return string  The escaped identifier
	 */
	private function escapeIdentifier($value)
	{
		$value = '"' . str_replace(
			array('"', '.'),
			array('',  '"."'),
			$value
		) . '"';
		if (in_array($this->type, array('oracle', 'db2'))) {
			$value = strtoupper($value);	
		}
		return $value;
	}
	
	
	/**
	 * Escapes an integer for use in SQL
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  integer $value  The integer to escape
	 * @return string  The escaped integer
	 */
	private function escapeInteger($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		if (!strlen($value)) {
			return 'NULL';
		}
		if (!preg_match('#^([+\-]?[0-9]+)(\.[0-9]*)?$#D', $value, $matches)) {
			return 'NULL';
		}
		return str_replace('+', '', $matches[1]);
	}
	
	
	/**
	 * Escapes a string for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` value will be returned as `'NULL'`.
	 * 
	 * @param  string $value  The string to escape
	 * @return string  The escaped string
	 */
	private function escapeString($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		$this->connect();
		
		if ($this->type == 'db2') {
			return "'" . str_replace("'", "''", $value) . "'";
		} elseif ($this->extension == 'mysql') {
			return "'" . mysql_real_escape_string($value, $this->connection) . "'";
		} elseif ($this->extension == 'mysqli') {
			return "'" . mysqli_real_escape_string($this->connection, $value) . "'";
		} elseif ($this->extension == 'pgsql') {
			return "'" . pg_escape_string($value) . "'";
		} elseif ($this->extension == 'sqlite') {
			return "'" . sqlite_escape_string($value) . "'";
		} elseif ($this->type == 'oracle') {
			return "'" . str_replace("'", "''", $value) . "'";
			
		} elseif ($this->type == 'mssql') {
			
			// If there are any non-ASCII characters, we need to escape
			if (preg_match('#[^\x00-\x7F]#', $value)) {
				preg_match_all('#.|^\z#us', $value, $characters);
				$output    = "";
				$last_type = NULL;
				foreach ($characters[0] as $character) {
					if (strlen($character) > 1) {
						$b = array_map('ord', str_split($character));
						switch (strlen($character)) {
							case 2:
								$bin = substr(decbin($b[0]), 3) .
										   substr(decbin($b[1]), 2);
								break;
							
							case 3:
								$bin = substr(decbin($b[0]), 4) .
										   substr(decbin($b[1]), 2) .
										   substr(decbin($b[2]), 2);
								break;
							
							// If it is a 4-byte character, MSSQL can't store it
							// so instead store a ?
							default:
								$output .= '?';
								continue;
						}
						if ($last_type == 'nchar') {
							$output .= '+';
						} elseif ($last_type == 'char') {
							$output .= "'+";
						}		
						$output .= "NCHAR(" . bindec($bin) . ")";
						$last_type = 'nchar';
					} else {
						if (!$last_type) {
							$output .= "'";
						} elseif ($last_type == 'nchar') {
							$output .= "+'";	
						}
						$output .= $character;
						// Escape single quotes
						if ($character == "'") {
							$output .= "'";
						}
						$last_type = 'char';
					}
				}
				if ($last_type == 'char') {
					$output .= "'";
				} elseif (!$last_type) {
					$output .= "''";	
				}
			
			// ASCII text is normal
			} else {
				$output = "'" . str_replace("'", "''", $value) . "'";
			}
			
			# a \ before a \r\n has to be escaped with another \
			return preg_replace('#(?<!\\\\)\\\\(?=\r\n)#', '\\\\\\\\', $output);
		
		} elseif ($this->extension == 'pdo') {
			return $this->connection->quote($value);
		}
	}
	
	
	/**
	 * Takes a SQL string and an array of values and replaces the placeholders with the value
	 * 
	 * @param string  $sql               The SQL string containing placeholders
	 * @param array   $values            An array of values to escape into the SQL
	 * @param boolean $unescape_percent  If %% should be translated to % - this should only be done once processing of the string is done
	 * @return string  The SQL with the values escaped into it
	 */
	private function escapeSQL($sql, $values, $unescape_percent)
	{
		$original_sql = $sql;
		$pieces = preg_split('#(?<!%)(%[lbdfristp])\b#', $sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
		
		$sql   = '';
		$value = array_shift($values);
		
		$missing_values = -1;
		
		foreach ($pieces as $piece) {
			switch ($piece) {
				case '%l':
					$callback = $this->escapeBlob;
					break;
				case '%b':
					$callback = $this->escapeBoolean;
					break;
				case '%d':
					$callback = $this->escapeDate;
					break;
				case '%f':
					$callback = $this->escapeFloat;
					break;
				case '%r':
					$callback = $this->escapeIdentifier;
					break;
				case '%i':
					$callback = $this->escapeInteger;
					break;
				case '%s':
					$callback = $this->escapeString;
					break;
				case '%t':
					$callback = $this->escapeTime;
					break;
				case '%p':
					$callback = $this->escapeTimestamp;
					break;
				default:
					if ($unescape_percent) {
						$piece = str_replace('%%', '%', $piece);
					}
					$sql .= $piece;
					continue 2;	
			}
			
			if (is_array($value)) {
				$sql .= join(', ', array_map($callback, $value));		
			} else {
				$sql .= call_user_func($callback, $value);
			}
					
			if (sizeof($values)) {
				$value = array_shift($values);
			} else {
				$value = NULL;
				$missing_values++;	
			}
		}
		
		if ($missing_values > 0) {
			throw new fProgrammerException(
				'%1$s value(s) are missing for the placeholders in: %2$s',
				$missing_values,
				$original_sql
			);	
		}
		
		if (sizeof($values)) {
			throw new fProgrammerException(
				'%1$s extra value(s) were passed for the placeholders in: %2$s',
				sizeof($values),
				$original_sql
			); 	
		}
		
		return $sql;
	}
	
	
	/**
	 * Escapes a time for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The time to escape
	 * @return string  The escaped time
	 */
	private function escapeTime($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		try {
			$value = new fTime($value);
			
			if ($this->type == 'mssql' || $this->type == 'oracle') {
				return "'" . $value->format('1970-01-01 H:i:s') . "'";	
			}
			
			return "'" . $value->format('H:i:s') . "'";
			
		} catch (fValidationException $e) {
			return 'NULL';
		}
	}
	
	
	/**
	 * Escapes a timestamp for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The timestamp to escape
	 * @return string  The escaped timestamp
	 */
	private function escapeTimestamp($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		try {
			$value = new fTimestamp($value);
			return "'" . $value->format('Y-m-d H:i:s') . "'";
			
		} catch (fValidationException $e) {
			return 'NULL';
		}
	}
	
	
	/**
	 * Executes one or more SQL queries without returning any results
	 * 
	 * @param  string|fStatement $statement  One or more SQL statements in a string or an fStatement prepared statement
	 * @param  mixed             $value      The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed             ...
	 * @return void
	 */
	public function execute($statement)
	{
		$args    = func_get_args();
		$params  = array_slice($args, 1);
		
		if (is_object($statement)) {
			return $this->run($statement, NULL, $params);	
		}
		
		$queries = $this->preprocess($statement, $params, FALSE);
		
		$output = array();
		foreach ($queries as $query) {
			$this->run($query);	
		}
	}


	/**
	 * Pulls quoted strings out into the values array for simpler processing
	 *
	 * @param  array $parts    The parts of the SQL - alternating SQL and quoted strings
	 * @param  array &$values  The value to be escaped into the SQL
	 * @return string  The SQL with all quoted string values extracted into the `$values` array
	 */
	private function extractStrings($parts, &$values)
	{
		$sql = '';

		$value_number = 0;
		foreach ($parts as $part) {
			// We leave blank strings in because Oracle treats them like NULL
			if ($part[0] == "'" && $part != "''") {
				$sql .= '%s';
				$value = str_replace("''", "'", substr($part, 1, -1));
				if ($this->type == 'postgresql') {
					$value = str_replace('\\\\', '\\', $value);
				}
				$values = array_merge(
					array_slice($values, 0, $value_number),
					array($value),
					array_slice($values, $value_number)
				);
				$value_number++;
			} else {
				$value_number += preg_match_all('#(?<!%)%[lbdfristp]\b#', $part, $trash);
				unset($trash);
				$sql .= $part;
			} 		
		}

		return $sql;
	}
	
	
	/**
	 * Returns the database connection resource or object
	 * 
	 * @return mixed  The database connection
	 */
	public function getConnection()
	{
		$this->connect();
		return $this->connection;
	}
	
	
	/**
	 * Gets the name of the database currently connected to
	 * 
	 * @return string  The name of the database currently connected to
	 */
	public function getDatabase()
	{
		return $this->database;
	}
	
	
	/**
	 * Gets the php extension being used
	 * 
	 * @internal
	 * 
	 * @return string  The php extension used for database interaction
	 */
	public function getExtension()
	{
		return $this->extension;
	}
	
	
	/**
	 * Gets the host for this database
	 * 
	 * @return string  The host
	 */
	public function getHost()
	{
		return $this->host;
	}
	
	
	/**
	 * Gets the port for this database
	 * 
	 * @return string  The port
	 */
	public function getPort()
	{
		return $this->port;
	}
	
	
	/**
	 * Gets the fSQLTranslation object used for translated queries
	 * 
	 * @return fSQLTranslation  The SQL translation object
	 */
	public function getSQLTranslation()
	{
		if (!$this->translation) { new fSQLTranslation($this); }
		return $this->translation;	
	}
	
	
	/**
	 * Gets the database type
	 * 
	 * @return string  The database type: `'mssql'`, `'mysql'`, `'postgresql'` or `'sqlite'`
	 */
	public function getType()
	{
		return $this->type;
	}
	
	
	/**
	 * Gets the username for this database
	 * 
	 * @return string  The username
	 */
	public function getUsername()
	{
		return $this->username;
	}


	/**
	 * Gets the version of the database system
	 * 
	 * @return string  The database system version
	 */
	public function getVersion()
	{
		if (isset($this->schema_info['version'])) {
			return $this->schema_info['version'];
		}

		switch ($this->type) {
			case 'db2':
				$sql = "SELECT REPLACE(service_level, 'DB2 v', '') FROM TABLE (sysproc.env_get_inst_info()) AS x";
				break;
			
			case 'mssql':
				$sql = "SELECT CAST(SERVERPROPERTY('ProductVersion') AS VARCHAR(500)) AS ProductVersion";
				break;

			case 'mysql':
				$sql = "SELECT version()";
				break;

			case 'oracle':
				$sql = "SELECT version FROM product_component_version";
				break;
			
			case 'postgresql':
				$sql = "SELECT regexp_replace(version(), E'^PostgreSQL +([0-9]+(\\\\.[0-9]+)*).*$', E'\\\\1')";
				break;

			case 'sqlite':
				$sql = "SELECT sqlite_version()";
				break;
		}

		$this->schema_info['version'] = preg_replace('#-?[a-z].*$#Di', '', $this->query($sql)->fetchScalar());
		return $this->schema_info['version'];
	}
	
	
	/**
	 * Will grab the auto incremented value from the last query (if one exists)
	 * 
	 * @param  fResult $result    The result object for the query
	 * @param  mixed   $resource  Only applicable for `pdo`, `oci8` and `sqlsrv` extentions or `mysqli` prepared statements - this is either the `PDOStatement` object, `mysqli_stmt` object or the `oci8` or `sqlsrv` resource
	 * @return void
	 */
	private function handleAutoIncrementedValue($result, $resource=NULL)
	{
		if (!preg_match('#^\s*INSERT\s+(?:INTO\s+)?(?:`|"|\[)?(["\w.]+)(?:`|"|\])?#i', $result->getSQL(), $table_match)) {
			$result->setAutoIncrementedValue(NULL);
			return;
		}
		$quoted_table = $table_match[1];
		$table        = str_replace('"', '', strtolower($table_match[1]));
		
		$insert_id = NULL;
		
		if ($this->type == 'oracle') {
			if (!isset($this->schema_info['sequences'])) {
				$sql = "SELECT
								LOWER(OWNER) AS \"SCHEMA\",
								LOWER(TABLE_NAME) AS \"TABLE\",
								TRIGGER_BODY
							FROM
								ALL_TRIGGERS
							WHERE
								TRIGGERING_EVENT LIKE 'INSERT%' AND
								STATUS = 'ENABLED' AND
								TRIGGER_NAME NOT LIKE 'BIN\$%' AND
								OWNER NOT IN (
									'SYS',
									'SYSTEM',
									'OUTLN',
									'ANONYMOUS',
									'AURORA\$ORB\$UNAUTHENTICATED',
									'AWR_STAGE',
									'CSMIG',
									'CTXSYS',
									'DBSNMP',
									'DIP',
									'DMSYS',
									'DSSYS',
									'EXFSYS',
									'FLOWS_020100',
									'FLOWS_FILES',
									'LBACSYS',
									'MDSYS',
									'ORACLE_OCM',
									'ORDPLUGINS',
									'ORDSYS',
									'PERFSTAT',
									'TRACESVR',
									'TSMSYS',
									'XDB'
								)";
								
				$this->schema_info['sequences'] = array();
				
				foreach ($this->query($sql) as $row) {
					if (preg_match('#SELECT\s+(["\w.]+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) {
						$table_name = $row['table'];
						if ($row['schema'] != strtolower($this->username)) {
							$table_name = $row['schema'] . '.' . $table_name;	
						}
						$this->schema_info['sequences'][$table_name] = array('sequence' => $matches[1], 'column' => str_replace('"', '', $matches[2]));
					}
				}
				
				if ($this->cache) {
					$this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);	
				}
			}
			
			if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+"?' . preg_quote($quoted_table, '#') . '"?\s+\([^\)]*?(\b|")' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '(\b|")#i', $result->getSQL())) {
				return;	
			}
			
			$insert_id_sql = "SELECT " . $this->schema_info['sequences'][$table]['sequence'] . ".currval AS INSERT_ID FROM dual";
		}
		
		if ($this->type == 'postgresql') {
			if (!isset($this->schema_info['sequences'])) {
				$sql = "SELECT
								pg_namespace.nspname AS \"schema\",
								pg_class.relname AS \"table\",
								pg_attribute.attname AS column
							FROM
								pg_attribute INNER JOIN
								pg_class ON pg_attribute.attrelid = pg_class.oid INNER JOIN
								pg_namespace ON pg_class.relnamespace = pg_namespace.oid INNER JOIN
								pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND pg_attribute.attnum = pg_attrdef.adnum
							WHERE
								NOT pg_attribute.attisdropped AND
								pg_attrdef.adsrc LIKE 'nextval(%'";
								
				$this->schema_info['sequences'] = array();
				
				foreach ($this->query($sql) as $row) {
					$table_name = strtolower($row['table']);
					if ($row['schema'] != 'public') {
						$table_name = $row['schema'] . '.' . $table_name;	
					}
					$this->schema_info['sequences'][$table_name] = $row['column'];
				}
				
				if ($this->cache) {
					$this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);	
				}	
			}
			
			if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+"?' . preg_quote($quoted_table, '#') . '"?\s+\([^\)]*?(\b|")' . preg_quote($this->schema_info['sequences'][$table], '#') . '(\b|")#i', $result->getSQL())) {
				return;
			} 		
		}
		
		if ($this->extension == 'ibm_db2') {
			$insert_id_res  = db2_exec($this->connection, "SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1");
			$insert_id_row  = db2_fetch_assoc($insert_id_res);
			$insert_id      = current($insert_id_row);
			db2_free_result($insert_id_res);
		
		} elseif ($this->extension == 'mssql') {
			$insert_id_res = mssql_query("SELECT @@IDENTITY AS insert_id", $this->connection);
			$insert_id     = mssql_result($insert_id_res, 0, 'insert_id');
			mssql_free_result($insert_id_res);
		
		} elseif ($this->extension == 'mysql') {
			$insert_id     = mysql_insert_id($this->connection);
		
		} elseif ($this->extension == 'mysqli') {
			if (is_object($resource)) {
				$insert_id = mysqli_stmt_insert_id($resource);
			} else {
				$insert_id = mysqli_insert_id($this->connection);
			}
		
		} elseif ($this->extension == 'oci8') {
			$oci_statement = oci_parse($this->connection, $insert_id_sql);
			oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS);
			$insert_id_row = oci_fetch_array($oci_statement, OCI_ASSOC);
			$insert_id = $insert_id_row['INSERT_ID'];
			oci_free_statement($oci_statement);
		
		} elseif ($this->extension == 'pgsql') {
			
			$insert_id_res = pg_query($this->connection, "SELECT lastval()");
			$insert_id_row = pg_fetch_assoc($insert_id_res);
			$insert_id = array_shift($insert_id_row);
			pg_free_result($insert_id_res);
		
		} elseif ($this->extension == 'sqlite') {
			$insert_id = sqlite_last_insert_rowid($this->connection);
		
		} elseif ($this->extension == 'sqlsrv') {
			$insert_id_res = sqlsrv_query($this->connection, "SELECT @@IDENTITY AS insert_id");
			$insert_id_row = sqlsrv_fetch_array($insert_id_res, SQLSRV_FETCH_ASSOC);
			$insert_id     = $insert_id_row['insert_id'];
			sqlsrv_free_stmt($insert_id_res);
		
		} elseif ($this->extension == 'pdo') {
			
			switch ($this->type) {
				case 'db2':
					$insert_id_statement = $this->connection->query("SELECT IDENTITY_VAL_LOCAL() FROM SYSIBM.SYSDUMMY1");
					$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
					$insert_id = array_shift($insert_id_row);
					$insert_id_statement->closeCursor();
					unset($insert_id_statement);
					break;
				
				case 'mssql':
					try {
						$insert_id_statement = $this->connection->query("SELECT @@IDENTITY AS insert_id");
						if (!$insert_id_statement) {
							throw new Exception();
						}
						
						$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
						$insert_id = array_shift($insert_id_row);
						
					} catch (Exception $e) {
						// If there was an error we don't have an insert id
					}
					break;
					
				case 'oracle':
					try {
						$insert_id_statement = $this->connection->query($insert_id_sql);
						if (!$insert_id_statement) {
							throw new Exception();
						}
						
						$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
						$insert_id = array_shift($insert_id_row);
						
					} catch (Exception $e) {
						// If there was an error we don't have an insert id
					}
					break;
				
				case 'postgresql':
					
					$insert_id_statement = $this->connection->query("SELECT lastval()");
					$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
					$insert_id = array_shift($insert_id_row);
					$insert_id_statement->closeCursor();
					unset($insert_id_statement);
					
					break;
		
				case 'mysql':
					$insert_id = $this->connection->lastInsertId();
					break;
		
				case 'sqlite':
					$insert_id = $this->connection->lastInsertId();
					break;
			}
		}
		
		$result->setAutoIncrementedValue($insert_id);
	}


	/**
	 * Handles connection errors
	 * 
	 * @param  array|string $errors  An array or string of error information
	 * @return void
	 */
	private function handleConnectionErrors($errors)
	{
		if (is_string($errors)) {
			$error = $errors;
		} else {
			$new_errors = array();
			foreach ($errors as $error) {
				$new_errors[] = isset($error['message']) ? $error['message'] : $error['string'];
			}
			$error = join("\n", $new_errors);
		}

		$connection_regexes = array(
			'db2'        => '#selectForConnectTimeout#',
			'mssql'      => '#(Connection refused|Can\'t assign requested address|Server is unavailable or does not exist|unable to connect|target machine actively refused it)#i',
			'mysql'      => '#(Can\'t connect to MySQL server|Lost connection to MySQL server at|Connection refused|Operation timed out|host has failed to respond)#',
			'oracle'     => '#(Connection refused|Can\'t assign requested address|no listener|unable to connect to)#',
			'postgresql' => '#(Connection refused|timeout expired|Network is unreachable|Can\'t assign requested address)#'
		);

		$authentication_regexes = array(
			'db2'        => '#USERNAME AND/OR PASSWORD INVALID#',
			'mssql'      => '#(Login incorrect|Adaptive Server connection failed|Login failed for user(?!.*Cannot open database))#is',
			'mysql'      => '#Access denied for user#',
			'oracle'     => '#invalid username/password#',
			'postgresql' => '#authentication failed#'
		);

		$database_regexes = array(
			'db2'        => '#database alias or database name#',
			'mssql'      => '#Could not locate entry in sysdatabases for database|Cannot open database|General SQL Server error: Check messages from the SQL Server#',
			'mysql'      => '#Unknown database#',
			'oracle'     => '#does not currently know of service requested#',
			'postgresql' => '#database "[^"]+" does not exist#'
		);

		if (isset($authentication_regexes[$this->type]) && preg_match($authentication_regexes[$this->type], $error)) {
			throw new fAuthorizationException(
				'Unable to connect to database - login credentials refused'
			);
		} elseif (isset($database_regexes[$this->type]) && preg_match($database_regexes[$this->type], $error)) {
			throw new fNotFoundException(
				'Unable to connect to database - database specified not found'
			);
		}

		// Provide a better error message if we can detect the hostname does not exist
		if (!preg_match('#^\d+\.\d+\.\d+\.\d+$#', $this->host)) {
			$ip_address = gethostbyname($this->host);
			if ($ip_address == $this->host) {
				throw new fConnectivityException(
					'Unable to connect to database - hostname not found'
				);
			}
		}

		if (isset($connection_regexes[$this->type]) && preg_match($connection_regexes[$this->type], $error)) {
			throw new fConnectivityException(
				'Unable to connect to database - connection refused or timed out'
			);
		}

		throw new fConnectivityException(
			"Unable to connect to database - unknown error:\n%1\$s",
			$error
		);
	}
	
	
	/**
	 * Handles a PHP error to extract error information for the mssql extension
	 * 
	 * @param  array $errors  An array of error information from fCore::stopErrorCapture()
	 * @return void
	 */
	private function handleErrors($errors)
	{
		if ($this->extension != 'mssql') {
			return;	
		}
		
		foreach ($errors as $error) {
			if (substr($error['string'], 0, 14) == 'mssql_query():') {
				if ($this->error) {
					$this->error .= " ";	
				}
				$this->error .= preg_replace('#^mssql_query\(\): ([^:]+: )?#', '', $error['string']);	
			}
		}
	}
	
	
	/**
	 * Makes sure each database and extension handles BEGIN, COMMIT and ROLLBACK 
	 * 
	 * @param  string|fStatement &$statement    The SQL to check for a transaction query
	 * @param  string            $result_class  The type of result object to create
	 * @return mixed  `FALSE` if normal processing should continue, otherwise an object of the type $result_class
	 */
	private function handleTransactionQueries(&$statement, $result_class)
	{
		if (is_object($statement)) {
			$sql = $statement->getSQL();
		} else {
			$sql = $statement;
		}

		// SQL Server supports transactions, but the statements are slightly different.
		// For the interest of convenience, we do simple transaction right here.
		if ($this->type == 'mssql') {
			if (preg_match('#^\s*(BEGIN|START(\s+TRANSACTION)?)\s*$#i', $sql)) {
				$statement = 'BEGIN TRANSACTION';
			} elseif (preg_match('#^\s*SAVEPOINT\s+("?\w+"?)\s*$#i', $sql, $match)) {
				$statement = 'SAVE TRANSACTION ' . $match[1];
			} elseif (preg_match('#^\s*ROLLBACK\s+TO\s+SAVEPOINT\s+("?\w+"?)\s*$#i', $sql, $match)) {
				$statement = 'ROLLBACK TRANSACTION ' . $match[1];
			}
		}
		
		$begin    = FALSE;
		$commit   = FALSE;
		$rollback = FALSE;
		
		// Track transactions since most databases don't support nesting
		if (preg_match('#^\s*(BEGIN|START)(\s+(TRAN|TRANSACTION|WORK))?\s*$#iD', $sql)) {
			if ($this->inside_transaction) {
				throw new fProgrammerException('A transaction is already in progress');
			}
			$this->inside_transaction = TRUE;
			$begin = TRUE;
			
		} elseif (preg_match('#^\s*COMMIT(\s+(TRAN|TRANSACTION|WORK))?\s*$#iD', $sql)) {
			if (!$this->inside_transaction) {
				throw new fProgrammerException('There is no transaction in progress');
			}
			$this->inside_transaction = FALSE;
			$commit = TRUE;
			
		} elseif (preg_match('#^\s*ROLLBACK(\s+(TRAN|TRANSACTION|WORK))?\s*$#iD', $sql)) {
			if (!$this->inside_transaction) {
				throw new fProgrammerException('There is no transaction in progress');
			}
			$this->inside_transaction = FALSE;
			$rollback = TRUE;
		
		// MySQL needs to use this construct for starting transactions when using LOCK tables
		} elseif ($this->type == 'mysql' && preg_match('#^\s*SET\s+autocommit\s*=\s*(0|1)#i', $sql, $match)) {
			$this->inside_transaction = TRUE;
			if ($match[1] == '0') {
				$this->schema_info['mysql_autocommit'] = TRUE;
			} else {
				unset($this->schema_info['mysql_autocommit']);
			}

		// We have to track LOCK TABLES for MySQL because UNLOCK TABLES only implicitly commits if LOCK TABLES was used
		} elseif ($this->type == 'mysql' && preg_match('#^\s*LOCK\s+TABLES#i', $sql)) {
			// This command always implicitly commits
			$this->inside_transaction = FALSE;
			$this->schema_info['mysql_lock_tables'] = TRUE;

		// MySQL has complex handling of UNLOCK TABLES
		} elseif ($this->type == 'mysql' && preg_match('#^\s*UNLOCK\s+TABLES#i', $sql)) {
			// This command only implicitly commits if LOCK TABLES was used
			if (isset($this->schema_info['mysql_lock_tables'])) {
				$this->inside_transaction = FALSE;
			}
			unset($this->schema_info['mysql_lock_tables']);

		// These databases issue implicit commit commands when the following statements are run
		} elseif ($this->type == 'mysql' && preg_match('#^\s*(ALTER|CREATE(?!\s+TEMPORARY)|DROP|RENAME|TRUNCATE|LOAD|UNLOCK|GRANT|REVOKE|SET\s+PASSWORD|CACHE|ANALYSE|CHECK|OPTIMIZE|REPAIR|FLUSH|RESET)\b#i', $sql)) {
			$this->inside_transaction = FALSE;

		} elseif ($this->type == 'oracle' && preg_match('#^\s*(CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|REPLACE|ANALYZE|AUDIT|COMMENT)\b#i', $sql)) {
			$this->inside_transaction = FALSE;
			
		} elseif ($this->type == 'db2' && preg_match('#^\s*CALL\s+SYSPROC\.ADMIN_CMD\(\'REORG\s+TABLE\b#i', $sql)) {
			$this->inside_transaction = FALSE;
			// It appears PDO tracks the transactions, but doesn't know about implicit commits
			if ($this->extension == 'pdo') {
				$this->connection->commit();
			}
		}

		// If MySQL autocommit it set to 0 a new transaction is automatically started
		if (!empty($this->schema_info['mysql_autocommit'])) {
			$this->inside_transaction = TRUE;
		}

		if (!$begin && !$commit && !$rollback) {
			return FALSE;
		}
		
		// The PDO, OCI8 and SQLSRV extensions require special handling through methods and functions
		$is_pdo     = $this->extension == 'pdo';
		$is_oci     = $this->extension == 'oci8';
		$is_sqlsrv  = $this->extension == 'sqlsrv';
		$is_ibm_db2 = $this->extension == 'ibm_db2';
		
		if (!$is_pdo && !$is_oci && !$is_sqlsrv && !$is_ibm_db2) {
			return FALSE;
		}
		
		$this->statement = $statement;
		
		// PDO seems to act weird if you try to start transactions through a normal query call
		if ($is_pdo) {
			try {
				$is_mssql  = $this->type == 'mssql';
				$is_oracle = $this->type == 'oracle';
				if ($begin) {
					// The SQL Server PDO object hasn't implemented transactions
					if ($is_mssql) {
						$this->connection->exec('BEGIN TRANSACTION');
					} elseif ($is_oracle) {
						$this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
					} else {
						$this->connection->beginTransaction();
					}
				
				} elseif ($commit) {
					if ($is_mssql) {
						$this->connection->exec('COMMIT');
					} elseif ($is_oracle) {
						$this->connection->exec('COMMIT');
						$this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
					} else  {
						$this->connection->commit();
					}
				
				} elseif ($rollback) {
					if ($is_mssql) {
						$this->connection->exec('ROLLBACK');
					} elseif ($is_oracle) {                 
						$this->connection->exec('ROLLBACK');
						$this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
					} else {
						$this->connection->rollBack();
					}
				}
				
			} catch (Exception $e) {
				
				$db_type_map = array(
					'db2'        => 'DB2',
					'mssql'      => 'MSSQL',
					'mysql'      => 'MySQL',
					'oracle'     => 'Oracle',
					'postgresql' => 'PostgreSQL',
					'sqlite'     => 'SQLite'
				);
				
				throw new fSQLException(
					'%1$s error (%2$s) in %3$s',
					$db_type_map[$this->type],
					$e->getMessage(),
					$sql
				);
			}
		
		} elseif ($is_oci) {
			if ($commit) {
				oci_commit($this->connection);
			} elseif ($rollback) {
				oci_rollback($this->connection);
			}
		
		} elseif ($is_sqlsrv) {
			if ($begin) {
				sqlsrv_begin_transaction($this->connection);
			} elseif ($commit) {
				sqlsrv_commit($this->connection);
			} elseif ($rollback) {
				sqlsrv_rollback($this->connection);
			}
			
		} elseif ($is_ibm_db2) {
			if ($begin) {
				db2_autocommit($this->connection, FALSE);
			} elseif ($commit) {
				db2_commit($this->connection);
				db2_autocommit($this->connection, TRUE);
			} elseif ($rollback) {
				db2_rollback($this->connection);
				db2_autocommit($this->connection, TRUE);
			}
		}
		
		if ($result_class) {
			$result = new $result_class($this);
			$result->setSQL($sql);
			$result->setResult(TRUE);
			return $result;
		}
		
		return TRUE;
	}
	
	
	/**
	 * Injects an fSQLTranslation object to handle translation
	 * 
	 * @internal
	 * 
	 * @param  fSQLTranslation $sql_translation  The SQL translation object
	 * @return void
	 */
	public function inject($sql_translation)
	{
		$this->translation = $sql_translation;
	}
	
	
	/**
	 * Will indicate if a transaction is currently in progress
	 * 
	 * @return boolean  If a transaction has been started and not yet rolled back or committed
	 */
	public function isInsideTransaction()
	{
		return $this->inside_transaction;
	}
	
	
	/**
	 * Creates a unique cache prefix to help prevent cache conflicts
	 * 
	 * @return string  The cache prefix to use
	 */
	private function makeCachePrefix()
	{
		if (!$this->cache_prefix) {
			$prefix  = 'fDatabase::' . $this->type . '::';
			if ($this->host) {
				$prefix .= $this->host . '::';
			}
			if ($this->port) {
				$prefix .= $this->port . '::';
			}
			$prefix .= $this->database . '::';
			if ($this->username) {
				$prefix .= $this->username . '::';
			}
			$this->cache_prefix = $prefix;
		}
		
		return $this->cache_prefix;
	}
	
	
	/**
	 * Executes a SQL statement
	 * 
	 * @param  string|fStatement $statement  The statement to perform
	 * @param  array             $params     The parameters for prepared statements
	 * @return void
	 */
	private function perform($statement, $params)
	{
		fCore::startErrorCapture();
		
		$extra = NULL;
		if (is_object($statement)) {
			$result = $statement->execute($params, $extra, $statement != $this->statement);
		} elseif ($this->extension == 'ibm_db2') {
			$result = db2_exec($this->connection, $statement, array('cursor' => DB2_FORWARD_ONLY));
		} elseif ($this->extension == 'mssql') {
			$result = mssql_query($statement, $this->connection);
		} elseif ($this->extension == 'mysql') {
			$result = mysql_unbuffered_query($statement, $this->connection);
		} elseif ($this->extension == 'mysqli') { 
			$result = mysqli_query($this->connection, $statement, MYSQLI_USE_RESULT);
		} elseif ($this->extension == 'oci8') {
			$extra  = oci_parse($this->connection, $statement);
			$result = oci_execute($extra, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS);
		} elseif ($this->extension == 'pgsql') {
			$result = pg_query($this->connection, $statement);
		} elseif ($this->extension == 'sqlite') {
			$result = sqlite_exec($this->connection, $statement, $extra);
		} elseif ($this->extension == 'sqlsrv') {
			$result = sqlsrv_query($this->connection, $statement);
		} elseif ($this->extension == 'pdo') {
			if ($this->type == 'mssql' && !fCore::checkOS('windows')) {
				// pdo_dblib is all messed up for return values from ->exec()
				// and even ->query(), but ->query() is closer to correct and
				// we use some heuristics to overcome the limitations
				$result = $this->connection->query($statement);
				if ($result instanceof PDOStatement) {
					$result->closeCursor();
					$extra = $result;
					$result = TRUE;
					if (preg_match('#^\s*EXEC(UTE)?\s+#i', $statement)) {
						$error_info = $extra->errorInfo();
						if (strpos($error_info[2], '(null) [0] (severity 0)') !== 0) {
							$result = FALSE;
						}
					}
				}
			} else {
				$result = $this->connection->exec($statement);
			}
		}
		$this->statement = $statement;
		
		$this->handleErrors(fCore::stopErrorCapture());

		// The mssql extension will sometimes not return FALSE even if there are errors
		if (strlen($this->error)) {
			$result = FALSE;
		}

		if ($this->extension == 'mssql' && $result) {
			$this->error = '';
		}
		
		if ($result === FALSE) {
			$this->checkForError($result, $extra, is_object($statement) ? $statement->getSQL() : $statement);
			
		} elseif (!is_bool($result) && $result !== NULL) {
			if ($this->extension == 'ibm_db2') {
				db2_free_result($result);
			} elseif ($this->extension == 'mssql') {
				mssql_free_result($result);
			} elseif ($this->extension == 'mysql') {
				mysql_free_result($result);
			} elseif ($this->extension == 'mysqli') { 
				mysqli_free_result($result);
			} elseif ($this->extension == 'oci8') {
				oci_free_statement($oci_statement);
			} elseif ($this->extension == 'pgsql') {
				pg_free_result($result);
			} elseif ($this->extension == 'sqlsrv') {
				sqlsrv_free_stmt($result);
			}
		}
	}
	
	
	/**
	 * Executes an SQL query
	 * 
	 * @param  string|fStatement $statement  The statement to perform
	 * @param  fResult           $result     The result object for the query
	 * @param  array             $params     The parameters for prepared statements
	 * @return void
	 */
	private function performQuery($statement, $result, $params)
	{
		fCore::startErrorCapture();
		
		$extra = NULL;
		if (is_object($statement)) {
			$statement->executeQuery($result, $params, $extra, $statement != $this->statement);
			
		} elseif ($this->extension == 'ibm_db2') {
			$extra = db2_exec($this->connection, $statement, array('cursor' => DB2_FORWARD_ONLY));
			if (is_resource($extra)) {
				$rows = array();
				while ($row = db2_fetch_assoc($extra)) {
					$rows[] = $row;	
				}
				$result->setResult($rows);
				unset($rows);
			} else { 
				$result->setResult($extra);	
			}
			
		} elseif ($this->extension == 'mssql') {
			$result->setResult(mssql_query($result->getSQL(), $this->connection));
			
		} elseif ($this->extension == 'mysql') {
			$result->setResult(mysql_query($result->getSQL(), $this->connection));

		} elseif ($this->extension == 'mysqli') {
			$result->setResult(mysqli_query($this->connection, $result->getSQL()));
			
		} elseif ($this->extension == 'oci8') {
			$extra = oci_parse($this->connection, $result->getSQL());
			if ($extra && oci_execute($extra, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) {
				oci_fetch_all($extra, $rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC);
				$result->setResult($rows);
				unset($rows);	
			} else {
				$result->setResult(FALSE);
			}
			
		} elseif ($this->extension == 'pgsql') {
			$result->setResult(pg_query($this->connection, $result->getSQL()));
			
		} elseif ($this->extension == 'sqlite') {
			$result->setResult(sqlite_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $extra));
			
		} elseif ($this->extension == 'sqlsrv') {
			$extra = sqlsrv_query($this->connection, $result->getSQL());
			if (is_resource($extra)) {
				$rows = array();
				while ($row = sqlsrv_fetch_array($extra, SQLSRV_FETCH_ASSOC)) {
					$rows[] = $row;
				}
				$result->setResult($rows);
				unset($rows);
			} else {
				$result->setResult($extra);
			}
			
		} elseif ($this->extension == 'pdo') {
			if (preg_match('#^\s*CREATE(\s+OR\s+REPLACE)?\s+TRIGGER#i', $result->getSQL())) {
				$this->connection->exec($result->getSQL());
				$extra = FALSE;
				$returned_rows = array();
			} else {
				$extra = $this->connection->query($result->getSQL());
				if (is_object($extra)) {
					// This fixes a segfault issue with blobs and fetchAll() for pdo_ibm
					if ($this->type == 'db2') {
						$returned_rows = array();
						while (($row = $extra->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
							foreach ($row as $key => $value) {
								if (is_resource($value)) {
									$row[$key] = stream_get_contents($value);
								}
							}
							$returned_rows[] = $row;
						}

					// pdo_dblib doesn't throw an exception on error when executing
					// a prepared statement when compiled against FreeTDS, so we have
					// to manually check the error info to see if something went wrong
					} elseif ($this->type == 'mssql' && !fCore::checkOS('windows') && preg_match('#^\s*EXEC(UTE)?\s+#i', $result->getSQL())) {
						$error_info = $extra->errorInfo();
						if ($error_info && strpos($error_info[2], '(null) [0] (severity 0)') !== 0) {
							$returned_rows = FALSE;
						}

					} else {
						$returned_rows = $extra->fetchAll(PDO::FETCH_ASSOC);
					}
				} else {
					$returned_rows = $extra;
				}
				
				// The pdo_pgsql driver likes to return empty rows equal to the number of affected rows for insert and deletes
				if ($this->type == 'postgresql' && $returned_rows && $returned_rows[0] == array()) {
					$returned_rows = array(); 		
				}
			}
			
			$result->setResult($returned_rows);
		}
		$this->statement = $statement;
		
		$this->handleErrors(fCore::stopErrorCapture());

		// The mssql extension will sometimes not return FALSE even if there are errors
		if (strlen($this->error) && strpos($this->error, 'WARNING!') !== 0) {
			$result->setResult(FALSE);
		}
		
		$this->checkForError($result, $extra);
		
		if ($this->extension == 'mssql') {
			$this->error = '';
		}
		
		if ($this->extension == 'ibm_db2') {
			$this->setAffectedRows($result, $extra);
			if ($extra && !is_object($statement)) {
				db2_free_result($extra);
			}
			
		} elseif ($this->extension == 'pdo') {
			$this->setAffectedRows($result, $extra);
			if ($extra && !is_object($statement)) {
				$extra->closeCursor();
			}
			
		} elseif ($this->extension == 'oci8') {
			$this->setAffectedRows($result, $extra);
			if ($extra && !is_object($statement)) {
				oci_free_statement($extra);
			}
			
		} elseif ($this->extension == 'sqlsrv') {
			$this->setAffectedRows($result, $extra);
			if ($extra && !is_object($statement)) {
				sqlsrv_free_stmt($extra);
			}
			
		} else {
			$this->setAffectedRows($result, $extra);
		}
		
		$this->setReturnedRows($result);
		
		$this->handleAutoIncrementedValue($result, $extra);
	}
	
	
	/**
	 * Executes an unbuffered SQL query
	 * 
	 * @param  string|fStatement $statement  The statement to perform
	 * @param  fUnbufferedResult $result     The result object for the query
	 * @param  array             $params     The parameters for prepared statements
	 * @return void
	 */
	private function performUnbufferedQuery($statement, $result, $params)
	{
		fCore::startErrorCapture();
		
		$extra = NULL;
		if (is_object($statement)) {
			$statement->executeUnbufferedQuery($result, $params, $extra, $statement != $this->statement);
		} elseif ($this->extension == 'ibm_db2') {
			$result->setResult(db2_exec($this->connection, $statement, array('cursor' => DB2_FORWARD_ONLY)));
		} elseif ($this->extension == 'mssql') {
			$result->setResult(mssql_query($result->getSQL(), $this->connection, 20));
		} elseif ($this->extension == 'mysql') {
			$result->setResult(mysql_unbuffered_query($result->getSQL(), $this->connection));
		} elseif ($this->extension == 'mysqli') { 
			$result->setResult(mysqli_query($this->connection, $result->getSQL(), MYSQLI_USE_RESULT));
		} elseif ($this->extension == 'oci8') {
			$extra = oci_parse($this->connection, $result->getSQL());
			if (oci_execute($extra, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) {
				$result->setResult($extra);
			} else {
				$result->setResult(FALSE);	
			}
		} elseif ($this->extension == 'pgsql') {
			$result->setResult(pg_query($this->connection, $result->getSQL()));
		} elseif ($this->extension == 'sqlite') {
			$result->setResult(sqlite_unbuffered_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $extra));
		} elseif ($this->extension == 'sqlsrv') {
			$result->setResult(sqlsrv_query($this->connection, $result->getSQL()));
		} elseif ($this->extension == 'pdo') {
			$result->setResult($this->connection->query($result->getSQL()));
		}
		$this->statement = $statement;
		
		$this->handleErrors(fCore::stopErrorCapture());
		
		$this->checkForError($result, $extra);
	}
	
	
	/**
	 * Prepares a single fStatement object to execute prepared statements
	 * 
	 * Identifier placeholders (%r) are not supported with prepared statements.
	 * In addition, multiple values can not be escaped by a placeholder - only
	 * a single value can be provided.
	 * 
	 * @param  string  $sql  The SQL to prepare
	 * @return fStatement  A prepared statement object that can be passed to ::query(), ::unbufferedQuery() or ::execute()
	 */
	public function prepare($sql)
	{
		return $this->prepareStatement($sql);	
	}
	
	
	/**
	 * Prepares a single fStatement object to execute prepared statements
	 * 
	 * Identifier placeholders (%r) are not supported with prepared statements.
	 * In addition, multiple values can not be escaped by a placeholder - only
	 * a single value can be provided.
	 * 
	 * @param  string  $sql        The SQL to prepare
	 * @param  boolean $translate  If the SQL should be translated using fSQLTranslation
	 * @return fStatement  A prepare statement object that can be passed to ::query(), ::unbufferedQuery() or ::execute()
	 */
	private function prepareStatement($sql, $translate=FALSE)
	{
		// Ensure an SQL statement was passed
		if (empty($sql)) {
			throw new fProgrammerException('No SQL statement passed');
		}
		
		// This is just to keep the callback method signature consistent
		$values = array();
		
		if ($this->hook_callbacks['unmodified']) {
			foreach ($this->hook_callbacks['unmodified'] as $callback) {
				$params = array(
					$this,
					&$sql,
					&$values
				);
				call_user_func_array($callback, $params);
			}
		}
		
		// Separate the SQL from quoted values
		$parts  = $this->splitSQL($sql);
		$new_parts = array();
		foreach ($parts as $part) {
			if ($part[0] == "'") {
				$new_parts[] = $part;
			} else {
				// We have to escape the placeholders so that the extraction of
				// string to %s placeholder doesn't mess up the creation of the
				// prepare statement
				$new_parts[] = str_replace('%', '%%', $part);
			}
		}
		$query = $this->extractStrings($new_parts, $values);
		
		if ($this->hook_callbacks['extracted']) {
			foreach ($this->hook_callbacks['extracted'] as $callback) {
				$params = array(
					$this,
					&$query,
					&$values
				);
				call_user_func_array($callback, $params);
			}
		}
		
		$untranslated_sql = NULL;
		if ($translate) {
			$untranslated_sql = $sql;
			$query = $this->getSQLTranslation()->translate(array($query));
			if (count($query) > 1) {
				throw new fProgrammerException(
					"The SQL statement %1$s can not be used as a prepared statement because translation turns it into multiple SQL statements",
					$untranslated_sql
				);
			}
			$query = current($query);
		}

		// Pull all of the real placeholders (%%) out and replace them with
		// %%s for sprintf() in fStatement. We have to use %% because we are
		// going to put the extracted string back into the statement via %s.
		$pieces       = preg_split('#(%%[lbdfistp])\b#', $query, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
		$placeholders = array();
		$new_query    = '';
		foreach ($pieces as $piece) {
			if (strlen($piece) == 3 && substr($piece, 0, 2) == '%%') {
				$placeholders[] = substr($piece, 1);
				$new_query     .= '%%s';
			} else {
				$new_query .= $piece;
			}		
		}
		$query = $new_query;
		
		// Unescape literal semicolons in the queries
		$query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query);
		
		$query = $this->escapeSQL($query, $values, TRUE);

		return new fStatement($this, $query, $placeholders, $untranslated_sql);
	}
	
	
	/**
	 * Preprocesses SQL by escaping values, spliting queries, cleaning escaped semicolons, fixing backslashed single quotes and translating
	 * 
	 * @internal
	 *
	 * @param  string  $sql                The SQL to process
	 * @param  array   $values             Literal values to escape into the SQL
	 * @param  boolean $translate          If the SQL should be translated
	 * @param  array   &$rollback_queries  MySQL doesn't allow transactions around `ALTER TABLE` statements, and some of those require multiple statements, so this is an array of "undo" SQL statements 
	 * @return array  The split out SQL queries, queries that have been translated will have a string key of a number, `:` and the original SQL, non-translated SQL will have a numeric key
	 */
	public function preprocess($sql, $values, $translate, &$rollback_queries=NULL)
	{
		$this->connect();
		
		// Ensure an SQL statement was passed
		if (empty($sql)) {
			throw new fProgrammerException('No SQL statement passed');
		}
		
		if ($this->hook_callbacks['unmodified']) {
			foreach ($this->hook_callbacks['unmodified'] as $callback) {
				$params = array(
					$this,
					&$sql,
					&$values
				);
				call_user_func_array($callback, $params);
			}
		}
		
		// Separate the SQL from quoted values
		$parts = $this->splitSQL($sql, $placeholders);
		
		// If the values were passed as a single array, this handles that
		if (count($values) == 1 && is_array($values[0]) && count($values[0]) == $placeholders) {
			$values = array_shift($values);	
		}
				
		$sql          = $this->extractStrings($parts, $values);
		$queries      = preg_split('#(?<!\\\\);#', $sql);
		$queries      = array_map('trim', $queries);
		$output       = array();
		$value_number = 0;
		foreach ($queries as $query) {
			if (!strlen($query)) {
				continue;
			}

			$sqlite_ddl   = $this->type == 'sqlite' && preg_match('#^\s*(ALTER\s+TABLE|CREATE\s+TABLE|COMMENT\s+ON)\s+#i', $query);
			$pieces       = preg_split('#(?<!%)(%[lbdfristp])\b#', $query, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
			$new_sql      = '';
			$query_values = array();
			
			$num = 0;
			foreach ($pieces as $piece) {
				
				// A placeholder
				if (strlen($piece) == 2 && $piece[0] == '%') {
					
					$value = $values[$value_number];
					
					// Here we put numbers for LIMIT and OFFSET into the SQL so they can be translated properly
					if ($piece == '%i' && preg_match('#\b(LIMIT|OFFSET)\s+#Di', $new_sql)) {
						$new_sql .= (int) $value;
						$value_number++;
					
					// Here we put blank strings back into the SQL so they can be translated for Oracle
					} elseif ($piece == '%s' && $value !== NULL && ((string) $value) == '') {
						$new_sql .= "''";
						$value_number++;
					
					// SQLite needs the literal string values for DDL statements
					} elseif ($piece == '%s' && $sqlite_ddl) {
						$new_sql .= $this->escapeString($value);
						$value_number++;
					
					} elseif ($piece == '%r') {
						if (is_array($value)) {
							$new_sql .= join(', ', array_map($this->escapeIdentifier, $value));	
						} else {
							$new_sql .= $this->escapeIdentifier($value);
						}
						$value_number++;
						
					// Other placeholder/value combos just get added
					} else {
						$value_number++;
						$new_sql .= '%' . $num . '$' . $piece[1];
						$num++;
						$query_values[] = $value;
					}
				
				// A piece of SQL
				} else {
					$new_sql .= $piece;	
				}
			}
			
			$query = $new_sql;

			if ($this->hook_callbacks['extracted']) {
				foreach ($this->hook_callbacks['extracted'] as $callback) {
					$params = array(
						$this,
						&$query,
						&$query_values
					);
					call_user_func_array($callback, $params);
				}
			}

			if ($translate) {
				$query_set = $this->getSQLTranslation()->translate(array($query), $rollback_queries);
			} else {
				$query_set = array($query);
			}

			foreach ($query_set as $key => $query) {				
				// Unescape literal semicolons in the queries
				$query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query);
				
				// Escape the values into the SQL
				if ($query_values && preg_match_all('#(?<!%)%(\d+)\$([lbdfristp])\b#', $query, $matches, PREG_SET_ORDER)) {
					// If we translated, we may need to shuffle values around
					if ($translate) {
						$new_values = array();
						foreach ($matches as $match) {
							$new_values[] = $query_values[$match[1]];
						}
						$query_values = $new_values;
					}
					$query = preg_replace('#(?<!%)%\d+\$([lbdfristp])\b#', '%\1', $query);
					$query = $this->escapeSQL($query, $query_values, TRUE);	
				}
				
				if (!is_numeric($key)) {
					$key_parts = explode(':', $key);
					$key = count($output) . ':' . $key_parts[1];
				} else {
					$key = count($output);
				}
				$output[$key] = $query;
			}
		}

		return $output;
	}
	
	
	/**
	 * Executes one or more SQL queries and returns the result(s)
	 * 
	 * @param  string|fStatement $statement  One or more SQL statements in a string or a single fStatement prepared statement
	 * @param  mixed             $value      The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed             ...
	 * @return fResult|array  The fResult object(s) for the query
	 */
	public function query($statement)
	{
		$args    = func_get_args();
		$params  = array_slice($args, 1);
		
		if (is_object($statement)) {
			return $this->run($statement, 'fResult', $params);	
		}
		
		$queries = $this->preprocess($statement, $params, FALSE);
		
		$output = array();
		foreach ($queries as $query) {
			$output[] = $this->run($query, 'fResult');	
		}
		
		return sizeof($output) == 1 ? $output[0] : $output;
	}
	
	
	/**
	 * Registers a callback for one of the various query hooks - multiple callbacks can be registered for each hook
	 * 
	 * The following hooks are available:
	 *  - `'unmodified'`: The original SQL passed to fDatabase, for prepared statements this is called just once before the fStatement object is created
	 *  - `'extracted'`: The SQL after all non-empty strings have been extracted and replaced with ordered sprintf-style placeholders
	 *  - `'run'`: After the SQL has been run
	 * 
	 * Methods for the `'unmodified'` hook should have the following signature:
	 * 
	 *  - **`$database`**:  The fDatabase instance
	 *  - **`&$sql`**:      The original, unedited SQL
	 *  - **`&$values`**:   The values to be escaped into the placeholders in the SQL
	 * 
	 * Methods for the `'extracted'` hook should have the following signature:
	 * 
	 *  - **`$database`**:  The fDatabase instance
	 *  - **`&$sql`**:      The SQL with all strings removed and replaced with `%1$s`-style placeholders
	 *  - **`&$values`**:   The values to be escaped into the placeholders in the SQL
	 * 
	 * The `extracted` hook is the best place to modify the SQL since there is
	 * no risk of breaking string literals. Please note that there may be empty
	 * strings (`''`) present in the SQL since some databases treat those as
	 * `NULL`.
	 * 
	 * Methods for the `'run'` hook should have the following signature:
	 * 
	 *  - **`$database`**:    The fDatabase instance
	 *  - **`$query`**:       The (string) SQL or `array(0 => {fStatement object}, 1 => {values array})` 
	 *  - **`$query_time`**:  The (float) number of seconds the query took
	 *  - **`$result`**       The fResult or fUnbufferedResult object, or `FALSE` if no result
	 * 
	 * @param  string   $hook      The hook to register for
	 * @param  callback $callback  The callback to register - see the method description for details about the method signature
	 * @return void
	 */
	public function registerHookCallback($hook, $callback)
	{
		$valid_hooks = array(
			'unmodified',
			'extracted',
			'run'
		);
		
		if (!in_array($hook, $valid_hooks)) {
			throw new fProgrammerException(
				'The hook specified, %1$s, should be one of: %2$s.',
				$hook,
				join(', ', $valid_hooks)
			);
		}
		
		$this->hook_callbacks[$hook][] = $callback;
	}
	
	
	/**
	 * Runs a single statement and times it, removes any old unbuffered queries before starting
	 * 
	 * @param  string|fStatement $statement    The SQL statement or prepared statement to execute
	 * @param  string            $result_type  The type of result object to return, fResult or fUnbufferedResult
	 * @return fResult|fUnbufferedResult  The result for the query
	 */
	private function run($statement, $result_type=NULL, $params=array())
	{
		if ($this->unbuffered_result) {
			$this->unbuffered_result->__destruct();
			$this->unbuffered_result = NULL;
		}
		
		$start_time = microtime(TRUE);	
		
		$result = $this->handleTransactionQueries($statement, $result_type);

		if (is_object($statement)) {
			$sql = $statement->getSQL();		
		} else {
			$sql = $statement;	
		}

		if (!$result) {
			if ($result_type) {
				$result = new $result_type($this, $this->type == 'mssql' ? $this->schema_info['character_set'] : NULL);
				$result->setSQL($sql);
				
				if ($result_type == 'fResult') {
					$this->performQuery($statement, $result, $params);
				} else {
					$this->performUnbufferedQuery($statement, $result, $params);	
				}
				
				if ($statement instanceof fStatement && $statement->getUntranslatedSQL()) {
					$result->setUntranslatedSQL($statement->getUntranslatedSQL());
				}
				
			} else {
				$this->perform($statement, $params);	
			}
		}
		
		// Write some debugging info
		$query_time = microtime(TRUE) - $start_time;
		$this->query_time += $query_time;
		if (fCore::getDebug($this->debug)) {
			fCore::debug(
				self::compose(
					'Query time was %1$s seconds for:%2$s',
					$query_time,
					"\n" . $sql
				),
				$this->debug
			);
		}
		
		if ($this->hook_callbacks['run']) {
			foreach ($this->hook_callbacks['run'] as $callback) {
				$callback_params = array(
					$this,
					is_object($statement) ? array($statement, $params) : $sql,
					$query_time,
					$result
				);
				call_user_func_array($callback, $callback_params);
			}
		}
		
		if ($result_type) {
			return $result;
		}
	}


	/**
	 * Takes an array of rollback statements to undo part of a set of queries which involve one that failed
	 *
	 * This is only used for MySQL since it is the only database that does not
	 * support transactions about `ALTER TABLE` statements, but that also
	 * requires more than one query to accomplish many `ALTER TABLE` tasks.
	 *
	 * @param  array   $rollback_statements  The SQL statements used to rollback `ALTER TABLE` statements
	 * @param  integer $start_number         The number query that failed - this is used to determine which rollback statements to run
	 * @return void
	 */
	private function runRollbackStatements($rollback_statements, $start_number)
	{
		if ($rollback_statements) {
			$rollback_statements = array_slice($rollback_statements, 0, $start_number);
			$rollback_statements = array_reverse($rollback_statements);
			foreach ($rollback_statements as $rollback_statement) {
				$this->run($rollback_statement);
			}
		}
	}
	
	
	/**
	 * Turns an array possibly containing objects into an array of all strings
	 * 
	 * @param  array $values  The array of values to scalarize
	 * @return array  The scalarized values
	 */
	private function scalarize($values)
	{
		$new_values = array();
		foreach ($values as $value) {
			if (is_object($value) && is_callable(array($value, '__toString'))) {
				$value = $value->__toString();
			} elseif (is_object($value)) {
				$value = (string) $value;	
			} elseif (is_array($value)) {
				$value = $this->scalarize($value);	
			}
			$new_values[] = $value;
		}
		return $new_values;	
	}
	
	
	/**
	 * Sets the number of rows affected by the query
	 * 
	 * @param  fResult $result    The result object for the query
	 * @param  mixed   $resource  Only applicable for `ibm_db2`, `pdo`, `oci8` and `sqlsrv` extentions or `mysqli` prepared statements - this is either the `PDOStatement` object, `mysqli_stmt` object or the `oci8` or `sqlsrv` resource
	 * @return void
	 */
	private function setAffectedRows($result, $resource=NULL)
	{
		if ($this->extension == 'ibm_db2') {
			$insert_update_delete = preg_match('#^\s*(INSERT|UPDATE|DELETE)\b#i', $result->getSQL());
			$result->setAffectedRows(!$insert_update_delete ? 0 : db2_num_rows($resource));
		} elseif ($this->extension == 'mssql') {
			$affected_rows_result = mssql_query('SELECT @@ROWCOUNT AS rows', $this->connection);
			$result->setAffectedRows((int) mssql_result($affected_rows_result, 0, 'rows'));
		} elseif ($this->extension == 'mysql') {
			$result->setAffectedRows(mysql_affected_rows($this->connection));
		} elseif ($this->extension == 'mysqli') {
			if (is_object($resource)) {
				$result->setAffectedRows($resource->affected_rows);
			} else {
				$result->setAffectedRows(mysqli_affected_rows($this->connection));
			}
		} elseif ($this->extension == 'oci8') {
			$result->setAffectedRows(oci_num_rows($resource));
		} elseif ($this->extension == 'pgsql') {
			$result->setAffectedRows(pg_affected_rows($result->getResult()));
		} elseif ($this->extension == 'sqlite') {
			$result->setAffectedRows(sqlite_changes($this->connection));
		} elseif ($this->extension == 'sqlsrv') {
			$result->setAffectedRows(sqlsrv_rows_affected($resource));
		} elseif ($this->extension == 'pdo') {
			// This fixes the fact that rowCount is not reset for non INSERT/UPDATE/DELETE statements
			try {
				if (!$resource || !$resource->fetch()) {
					throw new PDOException();
				}
				$result->setAffectedRows(0);
			} catch (PDOException $e) {
				// The SQLite PDO driver seems to return 1 when no rows are returned from a SELECT statement
				if ($this->type == 'sqlite' && $this->extension == 'pdo' && preg_match('#^\s*SELECT#i', $result->getSQL())) {
					$result->setAffectedRows(0);	
				} elseif (!$resource) {
					$result->setAffectedRows(0);
				} else {
					$result->setAffectedRows($resource->rowCount());
				}
			}
		}
	}
	
	
	/**
	 * Sets the number of rows returned by the query
	 * 
	 * @param  fResult $result  The result object for the query
	 * @return void
	 */
	private function setReturnedRows($result)
	{
		if (is_resource($result->getResult()) || is_object($result->getResult())) {
			if ($this->extension == 'mssql') {
				$result->setReturnedRows(mssql_num_rows($result->getResult()));
			} elseif ($this->extension == 'mysql') {
				$result->setReturnedRows(mysql_num_rows($result->getResult()));
			} elseif ($this->extension == 'mysqli') {
				$result->setReturnedRows(mysqli_num_rows($result->getResult()));
			} elseif ($this->extension == 'pgsql') {
				$result->setReturnedRows(pg_num_rows($result->getResult()));
			} elseif ($this->extension == 'sqlite') {
				$result->setReturnedRows(sqlite_num_rows($result->getResult()));
			}
		} elseif (is_array($result->getResult())) {
			$result->setReturnedRows(sizeof($result->getResult()));
		}
	}
	
	
	/**
	 * Splits SQL into pieces of SQL and quoted strings
	 * 
	 * @param  string  $sql            The SQL to split
	 * @param  integer &$placeholders  The number of placeholders in the SQL
	 * @return array  The pieces
	 */
	private function splitSQL($sql, &$placeholders=NULL)
	{
		// Fix \' in MySQL and PostgreSQL
		if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql, '\\') !== FALSE) {
			$sql = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql);	
		}

		$parts         = array();
		$temp_sql      = $sql;
		$start_pos     = 0;
		$inside_string = FALSE;
		do {
			$pos = strpos($temp_sql, "'", $start_pos);
			if ($pos !== FALSE) {
				if (!$inside_string) {
					$part          = substr($temp_sql, 0, $pos);
					$placeholders += preg_match_all('#(?<!%)%[lbdfristp]\b#', $part, $trash);
					unset($trash);
					$parts[]       = $part;
					$temp_sql      = substr($temp_sql, $pos);
					$start_pos     = 1;
					$inside_string = TRUE;
					 
				} elseif ($pos == strlen($temp_sql)) {
					$parts[]  = $temp_sql;
					$temp_sql = '';
					$pos = FALSE;	
				
				// Skip single-quote-escaped single quotes
				} elseif (strlen($temp_sql) > $pos+1 && $temp_sql[$pos+1] == "'") {
					$start_pos = $pos+2;
							
				} else {
					$parts[]   = substr($temp_sql, 0, $pos+1);
					$temp_sql  = substr($temp_sql, $pos+1);
					$start_pos = 0;
					$inside_string = FALSE;
				}
			}
		} while ($pos !== FALSE);
		if ($temp_sql) {
			$placeholders += preg_match_all('#(?<!%)%[lbdfristp]\b#', $temp_sql, $trash);
			unset($trash);
			$parts[] = $temp_sql;	
		}
		
		return $parts;	
	}
	
	
	/**
	 * Translates one or more SQL statements using fSQLTranslation and executes them without returning any results
	 * 
	 * @param  string $sql    One or more SQL statements
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return void
	 */
	public function translatedExecute($sql)
	{
		$args    = func_get_args();
		$queries = $this->preprocess(
			$sql,
			array_slice($args, 1),
			TRUE,
			$rollback_statements
		);
		
		try {
			$output = array();
			$i = 0;
			foreach ($queries as $i => $query) {
				$this->run($query);
				$i++;
			}
		} catch (fSQLException $e) {
			$this->runRollbackStatements($rollback_statements, $i);
			throw $e;
		}
	}
	
	
	/**
	 * Translates a SQL statement and creates an fStatement object from it
	 * 
	 * Identifier placeholders (%r) are not supported with prepared statements.
	 * In addition, multiple values can not be escaped by a placeholder - only
	 * a single value can be provided.
	 * 
	 * @param  string  $sql  The SQL to prepare
	 * @return fStatement  A prepared statement object that can be passed to ::query(), ::unbufferedQuery() or ::execute()
	 */
	public function translatedPrepare($sql)
	{
		return $this->prepareStatement($sql, TRUE);	
	}
	
	
	/**
	 * Translates one or more SQL statements using fSQLTranslation and executes them
	 * 
	 * @param  string $sql    One or more SQL statements
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return fResult|array  The fResult object(s) for the query
	 */
	public function translatedQuery($sql)
	{
		$args    = func_get_args();
		$queries = $this->preprocess(
			$sql,
			array_slice($args, 1),
			TRUE,
			$rollback_statements
		);
		
		try {
			$output = array();
			$i = 0;
			
			foreach ($queries as $key => $query) {
				$result = $this->run($query, 'fResult');
				if (!is_numeric($key)) {
					list($number, $original_query) = explode(':', $key, 2);
					$result->setUntranslatedSQL($original_query);
				}
				$output[] = $result;
				$i++;
			}
		} catch (fSQLException $e) {
			$this->runRollbackStatements($rollback_statements, $i);
			throw $e;
		}
		
		return sizeof($output) == 1 ? $output[0] : $output;
	}
	
	
	/**
	 * Executes a single SQL statement in unbuffered mode. This is optimal for
	 * large results sets since it does not load the whole result set into
	 * memory first. The gotcha is that only one unbuffered result can exist at
	 * one time. If another unbuffered query is executed, the old result will
	 * be deleted.
	 * 
	 * @param  string|fStatement $statement  A single SQL statement
	 * @param  mixed             $value      The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed             ...
	 * @return fUnbufferedResult  The result object for the unbuffered query
	 */
	public function unbufferedQuery($statement)
	{
		$args    = func_get_args();
		$params  = array_slice($args, 1);
		
		if (is_object($statement)) {
			$result = $this->run($statement, 'fUnbufferedResult', $params);
			
		} else {
			$queries = $this->preprocess($statement, $params, FALSE);
			
			if (sizeof($queries) > 1) {
				throw new fProgrammerException(
					'Only a single unbuffered query can be run at a time, however %d were passed',
					sizeof($queries)	
				);
			}
			
			$result = $this->run($queries[0], 'fUnbufferedResult');
		}
		
		$this->unbuffered_result = $result;
		
		return $result;
	}
	
	
	/**
	 * Translates the SQL statement using fSQLTranslation and then executes it
	 * in unbuffered mode. This is optimal for large results sets since it does
	 * not load the whole result set into memory first. The gotcha is that only
	 * one unbuffered result can exist at one time. If another unbuffered query
	 * is executed, the old result will be deleted.
	 * 
	 * @param  string $sql    A single SQL statement
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return fUnbufferedResult  The result object for the unbuffered query
	 */
	public function unbufferedTranslatedQuery($sql)
	{
		if (preg_match('#^\s*(ALTER|COMMENT|CREATE|DROP)\s+#i', $sql)) {
			throw new fProgrammerException(
				"The SQL provided, %1$s, appears to be a DDL (data definition language) SQL statement, which can not be run via %2$s because it may result in multiple SQL statements being run. Please use %3$s instead.",
				$sql,
				__CLASS__ . '::unbufferedTranslatedQuery()',
				__CLASS__ . '::translatedExecute()'
			);
		}

		$args    = func_get_args();
		$queries = $this->preprocess(
			$sql,
			array_slice($args, 1),
			TRUE
		);  
		
		if (sizeof($queries) > 1) {
			throw new fProgrammerException(
				'Only a single unbuffered query can be run at a time, however %d were passed',
				sizeof($queries)	
			);
		}
		
		$query_keys = array_keys($queries);
		$key        = $query_keys[0];
		list($number, $original_query) = explode(':', $key, 2);
		
		$result = $this->run($queries[$key], 'fUnbufferedResult');
		$result->setUntranslatedSQL($original_query);
		
		$this->unbuffered_result = $result;
		
		return $result;
	}
	
	
	/**
	 * Unescapes a value coming out of a database based on its data type
	 * 
	 * The valid data types are:
	 * 
	 *  - `'blob'` (or `'%l'`)
	 *  - `'boolean'` (or `'%b'`)
	 *  - `'date'` (or `'%d'`)
	 *  - `'float'` (or `'%f'`)
	 *  - `'integer'` (or `'%i'`)
	 *  - `'string'` (also `'%s'`, `'varchar'`, `'char'` or `'text'`)
	 *  - `'time'` (or `'%t'`)
	 *  - `'timestamp'` (or `'%p'`)
	 * 
	 * @param  string $data_type  The data type being unescaped - see method description for valid values
	 * @param  mixed  $value      The value or array of values to unescape
	 * @return mixed  The unescaped value
	 */
	public function unescape($data_type, $value)
	{
		if ($value === NULL) {
			return $value;	
		}
		
		$callback = NULL;
		
		switch ($data_type) {
			// Testing showed that strings tend to be most common,
			// and moving this to the top of the switch statement
			// improved performance on read-heavy pages
			case 'string':
			case 'varchar':
			case 'char':
			case 'text':
			case '%s':
				return $value;
			
			case 'boolean':
			case '%b':
				$callback = $this->unescapeBoolean;
				break;
				
			case 'date':
			case '%d':
				$callback = $this->unescapeDate;
				break;
				
			case 'float':
			case '%f':
				return $value;
				
			case 'integer':
			case '%i':
				return $value;
			
			case 'time':
			case '%t':
				$callback = $this->unescapeTime;
				break;
				
			case 'timestamp':
			case '%p':
				$callback = $this->unescapeTimestamp;
				break;
			
			case 'blob':
			case '%l':
				$callback = $this->unescapeBlob;
				break;
		}
		
		if ($callback) {
			if (is_array($value)) {
				return array_map($callback, $value);	
			}
			return call_user_func($callback, $value);
		}	
		
		throw new fProgrammerException(
			'Unknown data type, %1$s, specified. Must be one of: %2$s.',
			$data_type,
			'blob, %l, boolean, %b, date, %d, float, %f, integer, %i, string, %s, time, %t, timestamp, %p'
		);	
	}
	
	
	/**
	 * Unescapes a blob coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return binary  The binary data
	 */
	private function unescapeBlob($value)
	{
		$this->connect();
		
		if ($this->extension == 'pgsql') {
			return pg_unescape_bytea($value);
		} elseif ($this->extension == 'pdo' && is_resource($value)) {
			return stream_get_contents($value);
		} elseif ($this->extension == 'sqlite') {
			return pack('H*', $value);
		} else {
			return $value;
		}
	}
	
	
	/**
	 * Unescapes a boolean coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return boolean  The boolean
	 */
	private function unescapeBoolean($value)
	{
		return ($value === 'f' || !$value) ? FALSE : TRUE;
	}
	
	
	/**
	 * Unescapes a date coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return string  The date in YYYY-MM-DD format
	 */
	private function unescapeDate($value)
	{
		if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
			return $value->format('Y-m-d');
		} elseif ($this->type == 'mssql') {
			$value = preg_replace('#:\d{3}#', '', $value);
		}
		return date('Y-m-d', strtotime($value));
	}
	
	
	/**
	 * Unescapes a time coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return string  The time in `HH:MM:SS` format
	 */
	private function unescapeTime($value)
	{
		if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
			return $value->format('H:i:s');
		} elseif ($this->type == 'mssql') {
			$value = preg_replace('#:\d{3}#', '', $value);
		}
		return date('H:i:s', strtotime($value));
	}
	
	
	/**
	 * Unescapes a timestamp coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return string  The timestamp in `YYYY-MM-DD HH:MM:SS` format
	 */
	private function unescapeTimestamp($value)
	{
		if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
			return $value->format('Y-m-d H:i:s');
		} elseif ($this->type == 'mssql') {
			$value = preg_replace('#:\d{3}#', '', $value);
		}
		return date('Y-m-d H:i:s', strtotime($value));
	}
}



/**
 * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fDate.php.























































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
<?php
/**
 * Represents a date as a value object
 * 
 * @copyright  Copyright (c) 2008-2011 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fDate
 * 
 * @version    1.0.0b11
 * @changes    1.0.0b11  Fixed a method signature [wb, 2011-08-24]
 * @changes    1.0.0b10  Fixed a bug with the constructor not properly handling unix timestamps that are negative integers [wb, 2011-06-02]
 * @changes    1.0.0b9   Changed the `$date` attribute to be protected [wb, 2011-03-20]
 * @changes    1.0.0b8   Added the `$simple` parameter to ::getFuzzyDifference() [wb, 2010-03-15]
 * @changes    1.0.0b7   Added a call to fTimestamp::callUnformatCallback() in ::__construct() for localization support [wb, 2009-06-01]
 * @changes    1.0.0b6   Backwards compatibility break - Removed ::getSecondsDifference(), added ::eq(), ::gt(), ::gte(), ::lt(), ::lte() [wb, 2009-03-05]
 * @changes    1.0.0b5   Updated for new fCore API [wb, 2009-02-16]
 * @changes    1.0.0b4   Fixed ::__construct() to properly handle the 5.0 to 5.1 change in strtotime() [wb, 2009-01-21]
 * @changes    1.0.0b3   Added support for CURRENT_TIMESTAMP and CURRENT_DATE SQL keywords [wb, 2009-01-11]
 * @changes    1.0.0b2   Removed the adjustment amount check from ::adjust() [wb, 2008-12-31]
 * @changes    1.0.0b    The initial implementation [wb, 2008-02-10]
 */
class fDate
{
	/**
	 * Composes text using fText if loaded
	 * 
	 * @param  string  $message    The message to compose
	 * @param  mixed   $component  A string or number to insert into the message
	 * @param  mixed   ...
	 * @return string  The composed and possible translated message
	 */
	static protected function compose($message)
	{
		$args = array_slice(func_get_args(), 1);
		
		if (class_exists('fText', FALSE)) {
			return call_user_func_array(
				array('fText', 'compose'),
				array($message, $args)
			);
		} else {
			return vsprintf($message, $args);
		}
	}
	
	
	/**
	 * A timestamp of the date
	 * 
	 * @var integer
	 */
	protected $date;
	
	
	/**
	 * Creates the date to represent, no timezone is allowed since dates don't have timezones
	 * 
	 * @throws fValidationException  When `$date` is not a valid date
	 * 
	 * @param  fDate|object|string|integer $date  The date to represent, `NULL` is interpreted as today
	 * @return fDate
	 */
	public function __construct($date=NULL)
	{
		if ($date === NULL) {
			$timestamp = time();
		} elseif (is_numeric($date) && preg_match('#^-?\d+$#D', $date)) {
			$timestamp = (int) $date;
		} elseif (is_string($date) && in_array(strtoupper($date), array('CURRENT_TIMESTAMP', 'CURRENT_DATE'))) {
			$timestamp = time();
		} else {
			if (is_object($date) && is_callable(array($date, '__toString'))) {
				$date = $date->__toString();	
			} elseif (is_numeric($date) || is_object($date)) {
				$date = (string) $date;	
			}
			
			$date = fTimestamp::callUnformatCallback($date);
			
			$timestamp = strtotime(fTimestamp::fixISOWeek($date));
		}
		
		$is_51    = fCore::checkVersion('5.1');
		$is_valid = ($is_51 && $timestamp !== FALSE) || (!$is_51 && $timestamp !== -1);
		
		if (!$is_valid) {
			throw new fValidationException(
				'The date specified, %s, does not appear to be a valid date',
				$date
			);
		}
		
		$this->date = strtotime(date('Y-m-d 00:00:00', $timestamp));
	}
	
	
	/**
	 * All requests that hit this method should be requests for callbacks
	 * 
	 * @internal
	 * 
	 * @param  string $method  The method to create a callback for
	 * @return callback  The callback for the method requested
	 */
	public function __get($method)
	{
		return array($this, $method);		
	}
	
	
	/**
	 * Returns this date in `Y-m-d` format
	 * 
	 * @return string  The `Y-m-d` format of this date
	 */
	public function __toString()
	{
		return date('Y-m-d', $this->date);
	}
	
	
	/**
	 * Changes the date by the adjustment specified, only adjustments of a day or more will be made
	 * 
	 * @throws fValidationException  When `$adjustment` is not a relative date measurement
	 * 
	 * @param  string $adjustment  The adjustment to make
	 * @return fDate  The adjusted date
	 */
	public function adjust($adjustment)
	{
		$timestamp = strtotime($adjustment, $this->date);
		
		if ($timestamp === FALSE || $timestamp === -1) {
			throw new fValidationException(
				'The adjustment specified, %s, does not appear to be a valid relative date measurement',
				$adjustment
			);
		}
		
		return new fDate($timestamp);
	}
	
	
	/**
	 * If this date is equal to the date passed
	 * 
	 * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
	 * @return boolean  If this date is equal to the one passed
	 */
	public function eq($other_date=NULL)
	{
		$other_date = new fDate($other_date);
		return $this->date == $other_date->date;
	}
	
	
	/**
	 * Formats the date
	 * 
	 * @throws fValidationException  When a non-date formatting character is included in `$format`
	 * 
	 * @param  string $format  The [http://php.net/date date()] function compatible formatting string, or a format name from fTimestamp::defineFormat()
	 * @return string  The formatted date
	 */
	public function format($format)
	{
		$format = fTimestamp::translateFormat($format);
		
		$restricted_formats = 'aABcegGhHiIOPrsTuUZ';
		if (preg_match('#(?!\\\\).[' . $restricted_formats . ']#', $format)) {
			throw new fProgrammerException(
				'The formatting string, %1$s, contains one of the following non-date formatting characters: %2$s',
				$format,
				join(', ', str_split($restricted_formats))
			);
		}
		
		return fTimestamp::callFormatCallback(date($format, $this->date));
	}
	
	
	/**
	 * Returns the approximate difference in time, discarding any unit of measure but the least specific.
	 * 
	 * The output will read like:
	 * 
	 *  - "This date is `{return value}` the provided one" when a date it passed
	 *  - "This date is `{return value}`" when no date is passed and comparing with today
	 * 
	 * Examples of output for a date passed might be:
	 * 
	 *  - `'2 days after'`
	 *  - `'1 year before'`
	 *  - `'same day'`
	 * 
	 * Examples of output for no date passed might be:
	 * 
	 *  - `'2 days from now'`
	 *  - `'1 year ago'`
	 *  - `'today'`
	 * 
	 * You would never get the following output since it includes more than one unit of time measurement:
	 * 
	 *  - `'3 weeks and 1 day'`
	 *  - `'1 year and 2 months'`
	 * 
	 * Values that are close to the next largest unit of measure will be rounded up:
	 * 
	 *  - `6 days` would be represented as `1 week`, however `5 days` would not
	 *  - `29 days` would be represented as `1 month`, but `21 days` would be shown as `3 weeks`
	 * 
	 * @param  fDate|object|string|integer $other_date  The date to create the difference with, `NULL` is interpreted as today
	 * @param  boolean                     $simple      When `TRUE`, the returned value will only include the difference in the two dates, but not `from now`, `ago`, `after` or `before`
	 * @param  boolean                     |$simple
	 * @return string  The fuzzy difference in time between the this date and the one provided
	 */
	public function getFuzzyDifference($other_date=NULL, $simple=FALSE)
	{
		if (is_bool($other_date)) {
			$simple     = $other_date;
			$other_date = NULL;
		}
		
		$relative_to_now = FALSE;
		if ($other_date === NULL) {
			$relative_to_now = TRUE;
		}
		$other_date = new fDate($other_date);
		
		$diff = $this->date - $other_date->date;
		
		if (abs($diff) < 86400) {
			if ($relative_to_now) {
				return self::compose('today');
			}
			return self::compose('same day');
		}
		
		static $break_points = array();
		if (!$break_points) {
			$break_points = array(
				/* 5 days      */
				432000     => array(86400,    self::compose('day'),   self::compose('days')),
				/* 3 weeks     */
				1814400    => array(604800,   self::compose('week'),  self::compose('weeks')),
				/* 9 months    */
				23328000   => array(2592000,  self::compose('month'), self::compose('months')),
				/* largest int */
				2147483647 => array(31536000, self::compose('year'),  self::compose('years'))
			);
		}
		
		foreach ($break_points as $break_point => $unit_info) {
			if (abs($diff) > $break_point) { continue; }
			
			$unit_diff = round(abs($diff)/$unit_info[0]);
			$units     = fGrammar::inflectOnQuantity($unit_diff, $unit_info[1], $unit_info[2]);
			break;
		}
		
		if ($simple) {
			return self::compose('%1$s %2$s', $unit_diff, $units);
		}
		
		if ($relative_to_now) {
			if ($diff > 0) {
				return self::compose('%1$s %2$s from now', $unit_diff, $units);
			}
			
			return self::compose('%1$s %2$s ago', $unit_diff, $units);
		}
		
		if ($diff > 0) {
			return self::compose('%1$s %2$s after', $unit_diff, $units);
		}
		
		return self::compose('%1$s %2$s before', $unit_diff, $units);
	}
	
	
	/**
	 * If this date is greater than the date passed
	 * 
	 * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
	 * @return boolean  If this date is greater than the one passed
	 */
	public function gt($other_date=NULL)
	{
		$other_date = new fDate($other_date);
		return $this->date > $other_date->date;
	}
	
	
	/**
	 * If this date is greater than or equal to the date passed
	 * 
	 * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
	 * @return boolean  If this date is greater than or equal to the one passed
	 */
	public function gte($other_date=NULL)
	{
		$other_date = new fDate($other_date);
		return $this->date >= $other_date->date;
	}
	
	
	/**
	 * If this date is less than the date passed
	 * 
	 * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
	 * @return boolean  If this date is less than the one passed
	 */
	public function lt($other_date=NULL)
	{
		$other_date = new fDate($other_date);
		return $this->date < $other_date->date;
	}
	
	
	/**
	 * If this date is less than or equal to the date passed
	 * 
	 * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
	 * @return boolean  If this date is less than or equal to the one passed
	 */
	public function lte($other_date=NULL)
	{
		$other_date = new fDate($other_date);
		return $this->date <= $other_date->date;
	}
	
	
	/**
	 * Modifies the current date, creating a new fDate object
	 * 
	 * The purpose of this method is to allow for easy creation of a date
	 * based on this date. Below are some examples of formats to
	 * modify the current date:
	 * 
	 *  - `'Y-m-01'` to change the date to the first of the month
	 *  - `'Y-m-t'` to change the date to the last of the month
	 *  - `'Y-\W5-N'` to change the date to the 5th week of the year
	 * 
	 * @param  string $format  The current date will be formatted with this string, and the output used to create a new object
	 * @return fDate  The new date
	 */
	public function modify($format)
	{
	   return new fDate($this->format($format));
	}
}



/**
 * Copyright (c) 2008-2011 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fDirectory.php.







































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
<?php
/**
 * Represents a directory on the filesystem, also provides static directory-related methods
 * 
 * @copyright  Copyright (c) 2007-2011 Will Bond, others
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @author     Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fDirectory
 * 
 * @version    1.0.0b14
 * @changes    1.0.0b14  Fixed a bug in ::delete() where a non-existent method was being called on fFilesystem, added a permission check to ::delete() [wb, 2011-08-23]
 * @changes    1.0.0b13  Added the ::clear() method [wb, 2011-01-10]
 * @changes    1.0.0b12  Fixed ::scanRecursive() to not add duplicate entries for certain nested directory structures [wb, 2010-08-10]
 * @changes    1.0.0b11  Fixed ::scan() to properly add trailing /s for directories [wb, 2010-03-16]
 * @changes    1.0.0b10  BackwardsCompatibilityBreak - Fixed ::scan() and ::scanRecursive() to strip the current directory's path before matching, added support for glob style matching [wb, 2010-03-05]
 * @changes    1.0.0b9   Changed the way directories deleted in a filesystem transaction are handled, including improvements to the exception that is thrown [wb+wb-imarc, 2010-03-05]
 * @changes    1.0.0b8   Backwards Compatibility Break - renamed ::getFilesize() to ::getSize(), added ::move() [wb, 2009-12-16]
 * @changes    1.0.0b7   Fixed ::__construct() to throw an fValidationException when the directory does not exist [wb, 2009-08-21]
 * @changes    1.0.0b6   Fixed a bug where deleting a directory would prevent any future operations in the same script execution on a file or directory with the same path [wb, 2009-08-20]
 * @changes    1.0.0b5   Added the ability to skip checks in ::__construct() for better performance in conjunction with fFilesystem::createObject() [wb, 2009-08-06]
 * @changes    1.0.0b4   Refactored ::scan() to use the new fFilesystem::createObject() method [wb, 2009-01-21]
 * @changes    1.0.0b3   Added the $regex_filter parameter to ::scan() and ::scanRecursive(), fixed bug in ::scanRecursive() [wb, 2009-01-05]
 * @changes    1.0.0b2   Removed some unnecessary error suppresion operators [wb, 2008-12-11]
 * @changes    1.0.0b    The initial implementation [wb, 2007-12-21]
 */
class fDirectory
{
	// The following constants allow for nice looking callbacks to static methods
	const create        = 'fDirectory::create';
	const makeCanonical = 'fDirectory::makeCanonical';
	
	
	/**
	 * Creates a directory on the filesystem and returns an object representing it
	 * 
	 * The directory creation is done recursively, so if any of the parent
	 * directories do not exist, they will be created.
	 * 
	 * This operation will be reverted by a filesystem transaction being rolled back.
	 * 
	 * @throws fValidationException  When no directory was specified, or the directory already exists
	 * 
	 * @param  string  $directory  The path to the new directory
	 * @param  numeric $mode       The mode (permissions) to use when creating the directory. This should be an octal number (requires a leading zero). This has no effect on the Windows platform.
	 * @return fDirectory
	 */
	static public function create($directory, $mode=0777)
	{
		if (empty($directory)) {
			throw new fValidationException('No directory name was specified');
		}
		
		if (file_exists($directory)) {
			throw new fValidationException(
				'The directory specified, %s, already exists',
				$directory
			);
		}
		
		$parent_directory = fFilesystem::getPathInfo($directory, 'dirname');
		if (!file_exists($parent_directory)) {
			fDirectory::create($parent_directory, $mode);
		}
		
		if (!is_writable($parent_directory)) {
			throw new fEnvironmentException(
				'The directory specified, %s, is inside of a directory that is not writable',
				$directory
			);
		}
		
		mkdir($directory, $mode);
		
		$directory = new fDirectory($directory);
		
		fFilesystem::recordCreate($directory);
		
		return $directory;
	}
	
	
	/**
	 * Makes sure a directory has a `/` or `\` at the end
	 * 
	 * @param  string $directory  The directory to check
	 * @return string  The directory name in canonical form
	 */
	static public function makeCanonical($directory)
	{
		if (substr($directory, -1) != '/' && substr($directory, -1) != '\\') {
			$directory .= DIRECTORY_SEPARATOR;
		}
		return $directory;
	}
	
	
	/**
	 * A backtrace from when the file was deleted 
	 * 
	 * @var array
	 */
	protected $deleted = NULL;
	
	/**
	 * The full path to the directory
	 * 
	 * @var string
	 */
	protected $directory;
	
	
	/**
	 * Creates an object to represent a directory on the filesystem
	 * 
	 * If multiple fDirectory objects are created for a single directory,
	 * they will reflect changes in each other including rename and delete
	 * actions.
	 * 
	 * @throws fValidationException  When no directory was specified, when the directory does not exist or when the path specified is not a directory
	 * 
	 * @param  string  $directory    The path to the directory
	 * @param  boolean $skip_checks  If file checks should be skipped, which improves performance, but may cause undefined behavior - only skip these if they are duplicated elsewhere
	 * @return fDirectory
	 */
	public function __construct($directory, $skip_checks=FALSE)
	{
		if (!$skip_checks) {
			if (empty($directory)) {
				throw new fValidationException('No directory was specified');
			}
			
			if (!is_readable($directory)) {
				throw new fValidationException(
					'The directory specified, %s, does not exist or is not readable',
					$directory
				);
			}
			if (!is_dir($directory)) {
				throw new fValidationException(
					'The directory specified, %s, is not a directory',
					$directory
				);
			}
		}
		
		$directory = self::makeCanonical(realpath($directory));
		
		$this->directory =& fFilesystem::hookFilenameMap($directory);
		$this->deleted   =& fFilesystem::hookDeletedMap($directory);
		
		// If the directory is listed as deleted and we are not inside a transaction,
		// but we've gotten to here, then the directory exists, so we can wipe the backtrace
		if ($this->deleted !== NULL && !fFilesystem::isInsideTransaction()) {
			fFilesystem::updateDeletedMap($directory, NULL);
		}
	}
	
	
	/**
	 * All requests that hit this method should be requests for callbacks
	 * 
	 * @internal
	 * 
	 * @param  string $method  The method to create a callback for
	 * @return callback  The callback for the method requested
	 */
	public function __get($method)
	{
		return array($this, $method);		
	}
	
	
	/**
	 * Returns the full filesystem path for the directory
	 * 
	 * @return string  The full filesystem path
	 */
	public function __toString()
	{
		return $this->getPath();
	}
	
	
	/**
	 * Removes all files and directories inside of the directory
	 * 
	 * @return void
	 */
	public function clear()
	{
		if ($this->deleted) {
			return;	
		}
		
		foreach ($this->scan() as $file) {
			$file->delete();
		}
	}
	
	
	/**
	 * Will delete a directory and all files and directories inside of it
	 * 
	 * This operation will not be performed until the filesystem transaction
	 * has been committed, if a transaction is in progress. Any non-Flourish
	 * code (PHP or system) will still see this directory and all contents as
	 * existing until that point.
	 * 
	 * @return void
	 */
	public function delete()
	{
		if ($this->deleted) {
			return;	
		}

		if (!$this->getParent()->isWritable()) {
			throw new fEnvironmentException(
				'The directory, %s, can not be deleted because the directory containing it is not writable',
				$this->directory
			);
		}
		
		$files = $this->scan();
		
		foreach ($files as $file) {
			$file->delete();
		}
		
		// Allow filesystem transactions
		if (fFilesystem::isInsideTransaction()) {
			return fFilesystem::recordDelete($this);
		}
		
		rmdir($this->directory);
		
		fFilesystem::updateDeletedMap($this->directory, debug_backtrace());
		fFilesystem::updateFilenameMapForDirectory($this->directory, '*DELETED at ' . time() . ' with token ' . uniqid('', TRUE) . '* ' . $this->directory);
	}
	
	
	/**
	 * Gets the name of the directory
	 * 
	 * @return string  The name of the directory
	 */
	public function getName()
	{
		return fFilesystem::getPathInfo($this->directory, 'basename');
	}
	
	
	/**
	 * Gets the parent directory
	 * 
	 * @return fDirectory  The object representing the parent directory
	 */
	public function getParent()
	{
		$this->tossIfDeleted();
		
		$dirname = fFilesystem::getPathInfo($this->directory, 'dirname');
		
		if ($dirname == $this->directory) {
			throw new fEnvironmentException(
				'The current directory does not have a parent directory'
			);
		}
		
		return new fDirectory($dirname);
	}
	
	
	/**
	 * Gets the directory's current path
	 * 
	 * If the web path is requested, uses translations set with
	 * fFilesystem::addWebPathTranslation()
	 * 
	 * @param  boolean $translate_to_web_path  If the path should be the web path
	 * @return string  The path for the directory
	 */
	public function getPath($translate_to_web_path=FALSE)
	{
		$this->tossIfDeleted();
		
		if ($translate_to_web_path) {
			return fFilesystem::translateToWebPath($this->directory);
		}
		return $this->directory;
	}
	
	
	/**
	 * Gets the disk usage of the directory and all files and folders contained within
	 * 
	 * This method may return incorrect results if files over 2GB exist and the
	 * server uses a 32 bit operating system
	 * 
	 * @param  boolean $format          If the filesize should be formatted for human readability
	 * @param  integer $decimal_places  The number of decimal places to format to (if enabled)
	 * @return integer|string  If formatted, a string with filesize in b/kb/mb/gb/tb, otherwise an integer
	 */
	public function getSize($format=FALSE, $decimal_places=1)
	{
		$this->tossIfDeleted();
		
		$size = 0;
		
		$children = $this->scan();
		foreach ($children as $child) {
			$size += $child->getSize();
		}
		
		if (!$format) {
			return $size;
		}
		
		return fFilesystem::formatFilesize($size, $decimal_places);
	}
	
	
	/**
	 * Check to see if the current directory is writable
	 * 
	 * @return boolean  If the directory is writable
	 */
	public function isWritable()
	{
		$this->tossIfDeleted();
		
		return is_writable($this->directory);
	}
	
	
	/**
	 * Moves the current directory into a different directory
	 * 
	 * Please note that ::rename() will rename a directory in its current
	 * parent directory or rename it into a different parent directory.
	 * 
	 * If the current directory's name already exists in the new parent
	 * directory and the overwrite flag is set to false, the name will be
	 * changed to a unique name.
	 * 
	 * This operation will be reverted if a filesystem transaction is in
	 * progress and is later rolled back.
	 * 
	 * @throws fValidationException  When the new parent directory passed is not a directory, is not readable or is a sub-directory of this directory
	 * 
	 * @param  fDirectory|string $new_parent_directory  The directory to move this directory into
	 * @param  boolean           $overwrite             If the current filename already exists in the new directory, `TRUE` will cause the file to be overwritten, `FALSE` will cause the new filename to change
	 * @return fDirectory  The directory object, to allow for method chaining
	 */
	public function move($new_parent_directory, $overwrite)
	{
		if (!$new_parent_directory instanceof fDirectory) {
			$new_parent_directory = new fDirectory($new_parent_directory);
		}
		
		if (strpos($new_parent_directory->getPath(), $this->getPath()) === 0) {
			throw new fValidationException('It is not possible to move a directory into one of its sub-directories');	
		}
		
		return $this->rename($new_parent_directory->getPath() . $this->getName(), $overwrite);
	}
	
	
	/**
	 * Renames the current directory
	 * 
	 * This operation will NOT be performed until the filesystem transaction
	 * has been committed, if a transaction is in progress. Any non-Flourish
	 * code (PHP or system) will still see this directory (and all contained
	 * files/dirs) as existing with the old paths until that point.
	 * 
	 * @param  string  $new_dirname  The new full path to the directory or a new name in the current parent directory
	 * @param  boolean $overwrite    If the new dirname already exists, TRUE will cause the file to be overwritten, FALSE will cause the new filename to change
	 * @return void
	 */
	public function rename($new_dirname, $overwrite)
	{
		$this->tossIfDeleted();
		
		if (!$this->getParent()->isWritable()) {
			throw new fEnvironmentException(
				'The directory, %s, can not be renamed because the directory containing it is not writable',
				$this->directory
			);
		}
		
		// If the dirname does not contain any folder traversal, rename the dir in the current parent directory
		if (preg_match('#^[^/\\\\]+$#D', $new_dirname)) {
			$new_dirname = $this->getParent()->getPath() . $new_dirname;	
		}
		
		$info = fFilesystem::getPathInfo($new_dirname);
		
		if (!file_exists($info['dirname'])) {
			throw new fProgrammerException(
				'The new directory name specified, %s, is inside of a directory that does not exist',
				$new_dirname
			);
		}
		
		if (file_exists($new_dirname)) {
			if (!is_writable($new_dirname)) {
				throw new fEnvironmentException(
					'The new directory name specified, %s, already exists, but is not writable',
					$new_dirname
				);
			}
			if (!$overwrite) {
				$new_dirname = fFilesystem::makeUniqueName($new_dirname);
			}
		} else {
			$parent_dir = new fDirectory($info['dirname']);
			if (!$parent_dir->isWritable()) {
				throw new fEnvironmentException(
					'The new directory name specified, %s, is inside of a directory that is not writable',
					$new_dirname
				);
			}
		}
		
		rename($this->directory, $new_dirname);
		
		// Make the dirname absolute
		$new_dirname = fDirectory::makeCanonical(realpath($new_dirname));
		
		// Allow filesystem transactions
		if (fFilesystem::isInsideTransaction()) {
			fFilesystem::rename($this->directory, $new_dirname);
		}
		
		fFilesystem::updateFilenameMapForDirectory($this->directory, $new_dirname);
	}
	
	
	/**
	 * Performs a [http://php.net/scandir scandir()] on a directory, removing the `.` and `..` entries
	 * 
	 * If the `$filter` looks like a valid PCRE pattern - matching delimeters
	 * (a delimeter can be any non-alphanumeric, non-backslash, non-whitespace
	 * character) followed by zero or more of the flags `i`, `m`, `s`, `x`,
	 * `e`, `A`, `D`,  `S`, `U`, `X`, `J`, `u` - then
	 * [http://php.net/preg_match `preg_match()`] will be used.
	 * 
	 * Otherwise the `$filter` will do a case-sensitive match with `*` matching
	 * zero or more characters and `?` matching a single character.
	 * 
	 * On all OSes (even Windows), directories will be separated by `/`s when
	 * comparing with the `$filter`.
	 * 
	 * @param  string $filter  A PCRE or glob pattern to filter files/directories by path - directories can be detected by checking for a trailing / (even on Windows)
	 * @return array  The fFile (or fImage) and fDirectory objects for the files/directories in this directory
	 */
	public function scan($filter=NULL)
	{
		$this->tossIfDeleted();
		
		$files   = array_diff(scandir($this->directory), array('.', '..'));
		$objects = array();
		
		if ($filter && !preg_match('#^([^a-zA-Z0-9\\\\\s]).*\1[imsxeADSUXJu]*$#D', $filter)) {
			$filter = '#^' . strtr(
				preg_quote($filter, '#'),
				array(
					'\\*' => '.*',
					'\\?' => '.'
				)
			) . '$#D';
		}
		
		natcasesort($files);
		
		foreach ($files as $file) {
			if ($filter) {
				$test_path = (is_dir($this->directory . $file)) ? $file . '/' : $file;
				if (!preg_match($filter, $test_path)) {
					continue;
				}
			}
			
			$objects[] = fFilesystem::createObject($this->directory . $file);
		}
		
		return $objects;
	}
	
	
	/**
	 * Performs a **recursive** [http://php.net/scandir scandir()] on a directory, removing the `.` and `..` entries
	 * 
	 * @param  string $filter  A PCRE or glob pattern to filter files/directories by path - see ::scan() for details
	 * @return array  The fFile (or fImage) and fDirectory objects for the files/directories (listed recursively) in this directory
	 */
	public function scanRecursive($filter=NULL)
	{
		$this->tossIfDeleted();
		
		$objects = $this->scan();
		
		for ($i=0; $i < sizeof($objects); $i++) {
			if ($objects[$i] instanceof fDirectory) {
				array_splice($objects, $i+1, 0, $objects[$i]->scan());
			}
		}
		
		if ($filter) {
			if (!preg_match('#^([^a-zA-Z0-9\\\\\s*?^$]).*\1[imsxeADSUXJu]*$#D', $filter)) {
				$filter = '#^' . strtr(
					preg_quote($filter, '#'),
					array(
						'\\*' => '.*',
						'\\?' => '.'
					)
				) . '$#D';
			}
			
			$new_objects  = array();
			$strip_length = strlen($this->getPath());
			foreach ($objects as $object) {
				$test_path = substr($object->getPath(), $strip_length);
				$test_path = str_replace(DIRECTORY_SEPARATOR, '/', $test_path);
				if (!preg_match($filter, $test_path)) {
					continue;	
				}	
				$new_objects[] = $object;
			}
			$objects = $new_objects;
		}
		
		return $objects;
	}
	
	
	/**
	 * Throws an exception if the directory has been deleted
	 * 
	 * @return void
	 */
	protected function tossIfDeleted()
	{
		if ($this->deleted) {
			throw new fProgrammerException(
				"The action requested can not be performed because the directory has been deleted\n\nBacktrace for fDirectory::delete() call:\n%s",
				fCore::backtrace(0, $this->deleted)
			);
		}
	}
}



/**
 * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fEmail.php.



























































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
<?php
/**
 * Allows creating and sending a single email containing plaintext, HTML, attachments and S/MIME encryption
 * 
 * Please note that this class uses the [http://php.net/function.mail mail()]
 * function by default. Developers that are sending multiple emails, or need
 * SMTP support, should use fSMTP with this class.
 * 
 * This class is implemented to use the UTF-8 character encoding. Please see
 * http://flourishlib.com/docs/UTF-8 for more information.
 * 
 * @copyright  Copyright (c) 2008-2011 Will Bond, others
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @author     Bill Bushee, iMarc LLC [bb-imarc] <bill@imarc.net>
 * @author     netcarver [n] <fContrib@netcarving.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fEmail
 * 
 * @version    1.0.0b30
 * @changes    1.0.0b30  Changed methods to return instance for method chaining [n, 2011-09-12]
 * @changes    1.0.0b29  Changed ::combineNameEmail() to be a static method and to be exposed publicly for use by other classes [wb, 2011-07-26]
 * @changes    1.0.0b28  Fixed ::addAttachment() and ::addRelatedFile() to properly handle duplicate filenames [wb, 2011-05-17]
 * @changes    1.0.0b27  Fixed a bug with generating FQDNs on some Windows machines [wb, 2011-02-24]
 * @changes    1.0.0b26  Added ::addCustomerHeader() [wb, 2011-02-02]
 * @changes    1.0.0b25  Fixed a bug with finding the FQDN on non-Windows machines [wb, 2011-01-19]
 * @changes    1.0.0b24  Backwards Compatibility Break - the `$contents` parameter of ::addAttachment() is now first instead of third, ::addAttachment() will now accept fFile objects for the `$contents` parameter, added ::addRelatedFile() [wb, 2010-12-01]
 * @changes    1.0.0b23  Fixed a bug on Windows where emails starting with a `.` would have the `.` removed [wb, 2010-09-11]
 * @changes    1.0.0b22  Revamped the FQDN code and added ::getFQDN() [wb, 2010-09-07]
 * @changes    1.0.0b21  Added a check to prevent permissions warnings when getting the FQDN on Windows machines [wb, 2010-09-02]
 * @changes    1.0.0b20  Fixed ::send() to only remove the name of a recipient when dealing with the `mail()` function on Windows and to leave it when using fSMTP [wb, 2010-06-22]
 * @changes    1.0.0b19  Changed ::send() to return the message id for the email, fixed the email regexes to require [] around IPs [wb, 2010-05-05]
 * @changes    1.0.0b18  Fixed the name of the static method ::unindentExpand() [wb, 2010-04-28]
 * @changes    1.0.0b17  Added the static method ::unindentExpand() [wb, 2010-04-26]
 * @changes    1.0.0b16  Added support for sending emails via fSMTP [wb, 2010-04-20]
 * @changes    1.0.0b15  Added the `$unindent_expand_constants` parameter to ::setBody(), added ::loadBody() and ::loadHTMLBody(), fixed HTML emails with attachments [wb, 2010-03-14]
 * @changes    1.0.0b14  Changed ::send() to not double `.`s at the beginning of lines on Windows since it seemed to break things rather than fix them [wb, 2010-03-05]
 * @changes    1.0.0b13  Fixed the class to work when safe mode is turned on [wb, 2009-10-23]
 * @changes    1.0.0b12  Removed duplicate MIME-Version headers that were being included in S/MIME encrypted emails [wb, 2009-10-05]
 * @changes    1.0.0b11  Updated to use the new fValidationException API [wb, 2009-09-17]
 * @changes    1.0.0b10  Fixed a bug with sending both an HTML and a plaintext body [bb-imarc, 2009-06-18]
 * @changes    1.0.0b9   Fixed a bug where the MIME headers were not being set for all emails [wb, 2009-06-12]
 * @changes    1.0.0b8   Added the method ::clearRecipients() [wb, 2009-05-29]
 * @changes    1.0.0b7   Email names with UTF-8 characters are now properly encoded [wb, 2009-05-08]
 * @changes    1.0.0b6   Fixed a bug where <> quoted email addresses in validation messages were not showing [wb, 2009-03-27]
 * @changes    1.0.0b5   Updated for new fCore API [wb, 2009-02-16]
 * @changes    1.0.0b4   The recipient error message in ::validate() no longer contains a typo [wb, 2009-02-09]
 * @changes    1.0.0b3   Fixed a bug with missing content in the fValidationException thrown by ::validate() [wb, 2009-01-14]
 * @changes    1.0.0b2   Fixed a few bugs with sending S/MIME encrypted/signed emails [wb, 2009-01-10]
 * @changes    1.0.0b    The initial implementation [wb, 2008-06-23]
 */
class fEmail
{
	// The following constants allow for nice looking callbacks to static methods
	const combineNameEmail = 'fEmail::combineNameEmail';
	const fixQmail         = 'fEmail::fixQmail';
	const getFQDN          = 'fEmail::getFQDN';
	const reset            = 'fEmail::reset';
	const unindentExpand   = 'fEmail::unindentExpand';
	
	
	/**
	 * A regular expression to match an email address, exluding those with comments and folding whitespace
	 * 
	 * The matches will be:
	 *  
	 *  - `[0]`: The whole email address
	 *  - `[1]`: The name before the `@`
	 *  - `[2]`: The domain/ip after the `@`
	 * 
	 * @var string
	 */
	const EMAIL_REGEX = '~^[ \t]*(                                                                      # Allow leading whitespace
						   (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")                       # An "atom" or a quoted string
						   (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*    # A . plus another "atom" or a quoted string, any number of times
						 )@(                                                                            # The @ symbol
						   (?:[a-z0-9\\-]+\.)+[a-z]{2,}|                                                # Domain name
						   \[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\]  # (or) IP addresses
						 )[ \t]*$~ixD';                                                                 # Allow Trailing whitespace
	
	/**
	 * A regular expression to match a `name <email>` string, exluding those with comments and folding whitespace
	 * 
	 * The matches will be:
	 * 
	 *  - `[0]`: The whole name and email address
	 *  - `[1]`: The name
	 *  - `[2]`: The whole email address
	 *  - `[3]`: The email username before the `@`
	 *  - `[4]`: The email domain/ip after the `@`
	 * 
	 * @var string
	 */
	const NAME_EMAIL_REGEX = '~^[ \t]*(                                                                            # Allow leading whitespace
								(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*)                 # An "atom" or a quoted string
								(?:\.?[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+[ \t]*|"[^"\\\\\n\r]+"[ \t]*))*)  # Another "atom" or a quoted string or a . followed by one of those, any number of times
							  [ \t]*<[ \t]*((                                                                      # The < encapsulating the email address
								(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")                             # An "atom" or a quoted string
								(?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*          # A . plus another "atom" or a quoted string, any number of times
							  )@(                                                                                  # The @ symbol
								(?:[a-z0-9\\-]+\.)+[a-z]{2,}|                                                      # Domain nam
								\[(?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])\]        # (or) IP addresses
							  ))[ \t]*>[ \t]*$~ixD';                                                               # Closing > and trailing whitespace
	
	
	/**
	 * Flags if the class should convert `\r\n` to `\n` for qmail. This makes invalid email headers that may work.
	 * 
	 * @var boolean
	 */
	static private $convert_crlf  = FALSE;
	
	/**
	 * The local fully-qualified domain name
	 */
	static private $fqdn;
	
	/**
	 * Flags if the class should use [http://php.net/popen popen()] to send mail via sendmail
	 * 
	 * @var boolean
	 */
	static private $popen_sendmail = FALSE;


	/**
	 * Turns a name and email into a `"name" <email>` string, or just `email` if no name is provided
	 * 
	 * This method will remove newline characters from the name and email, and
	 * will remove any backslash (`\`) and double quote (`"`) characters from
	 * the name.
	 * 
	 * @internal
	 *
	 * @param  string $name   The name associated with the email address
	 * @param  string $email  The email address
	 * @return string  The '"name" <email>' or 'email' string
	 */
	static public function combineNameEmail($name, $email)
	{
		// Strip lower ascii character since they aren't useful in email addresses
		$email = preg_replace('#[\x0-\x19]+#', '', $email);
		$name  = preg_replace('#[\x0-\x19]+#', '', $name);
		
		if (!$name) {
			return $email;
		}
		
		// If the name contains any non-ascii bytes or stuff not allowed
		// in quoted strings we just make an encoded word out of it
		if (preg_replace('#[\x80-\xff\x5C\x22]#', '', $name) != $name) {
			// The longest header name that will contain email addresses is
			// "Bcc: ", which is 5 characters long
			$name = self::makeEncodedWord($name, 5);
		} else {
			$name = '"' . $name . '"';	
		}
		
		return $name . ' <' . $email . '>';
	}
	
	
	/**
	 * Composes text using fText if loaded
	 * 
	 * @param  string  $message    The message to compose
	 * @param  mixed   $component  A string or number to insert into the message
	 * @param  mixed   ...
	 * @return string  The composed and possible translated message
	 */
	static protected function compose($message)
	{
		$args = array_slice(func_get_args(), 1);
		
		if (class_exists('fText', FALSE)) {
			return call_user_func_array(
				array('fText', 'compose'),
				array($message, $args)
			);
		} else {
			return vsprintf($message, $args);
		}
	}
	
	
	/**
	 * Sets the class to try and fix broken qmail implementations that add `\r` to `\r\n`
	 * 
	 * Before trying to fix qmail with this method, please try using fSMTP
	 * to connect to `localhost` and pass the fSMTP object to ::send().
	 * 
	 * @return void
	 */
	static public function fixQmail()
	{
		if (fCore::checkOS('windows')) {
			return;
		}
		
		$sendmail_command = ini_get('sendmail_path');
		
		if (!$sendmail_command) {
			self::$convert_crlf = TRUE;
			trigger_error(
				self::compose('The proper fix for sending through qmail is not possible since the sendmail path is not set'),
				E_USER_WARNING
			);
			trigger_error(
				self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
				E_USER_WARNING
			);
		}
		
		$sendmail_command_parts = explode(' ', $sendmail_command, 2);
		
		$sendmail_path   = $sendmail_command_parts[0];
		$sendmail_dir    = pathinfo($sendmail_path, PATHINFO_DIRNAME);
		$sendmail_params = (isset($sendmail_command_parts[1])) ? $sendmail_command_parts[1] : '';
		
		// Check to see if we can run sendmail via popen
		$executable = FALSE;
		$safe_mode  = FALSE;
		
		if (!in_array(strtolower(ini_get('safe_mode')), array('0', '', 'off'))) {
			$safe_mode = TRUE;
			$exec_dirs = explode(';', ini_get('safe_mode_exec_dir'));
			foreach ($exec_dirs as $exec_dir) {
				if (stripos($sendmail_dir, $exec_dir) !== 0) {
					continue;
				}
				if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
					$executable = TRUE;
				}
			}
			
		} else {
			if (file_exists($sendmail_path) && is_executable($sendmail_path)) {
				$executable = TRUE;
			}
		}
		
		if ($executable) {
			self::$popen_sendmail = TRUE;
		} else {
			self::$convert_crlf   = TRUE;
			if ($safe_mode) {
				trigger_error(
					self::compose('The proper fix for sending through qmail is not possible since safe mode is turned on and the sendmail binary is not in one of the paths defined by the safe_mode_exec_dir ini setting'),
					E_USER_WARNING
				);
				trigger_error(
					self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
					E_USER_WARNING
				);
			} else {
				trigger_error(
					self::compose('The proper fix for sending through qmail is not possible since the sendmail binary could not be found or is not executable'),
					E_USER_WARNING
				);
				trigger_error(
					self::compose('Trying to fix qmail by converting all \r\n to \n. This will cause invalid (but possibly functioning) email headers to be generated.'),
					E_USER_WARNING
				);
			}
		}
	}
	
	
	/**
	 * Returns the fully-qualified domain name of the server
	 * 
	 * @internal
	 * 
	 * @return string  The fully-qualified domain name of the server
	 */
	static public function getFQDN()
	{
		if (self::$fqdn !== NULL) {
			return self::$fqdn;
		}
		
		if (isset($_ENV['HOST'])) {
			self::$fqdn = $_ENV['HOST'];
		}
		if (strpos(self::$fqdn, '.') === FALSE && isset($_ENV['HOSTNAME'])) {
			self::$fqdn = $_ENV['HOSTNAME'];
		}
		if (strpos(self::$fqdn, '.') === FALSE) {
			self::$fqdn = php_uname('n');
		}
		
		if (strpos(self::$fqdn, '.') === FALSE) {
			
			$can_exec = !in_array('exec', array_map('trim', explode(',', ini_get('disable_functions')))) && !ini_get('safe_mode');
			if (fCore::checkOS('linux') && $can_exec) {
				self::$fqdn = trim(shell_exec('hostname --fqdn'));
				
			} elseif (fCore::checkOS('windows')) {
				$shell = new COM('WScript.Shell');
				$tcpip_key = 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip';
				try {
					$domain = $shell->RegRead($tcpip_key . '\Parameters\NV Domain');
				} catch (com_exception $e) {
					try {
						$domain = $shell->RegRead($tcpip_key . '\Parameters\DhcpDomain');
					} catch (com_exception $e) {
						try {
							$adapters = $shell->RegRead($tcpip_key . '\Linkage\Route');
							foreach ($adapters as $adapter) {
								if ($adapter[0] != '{') { continue; }
								try {
									$domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\Domain');
								} catch (com_exception $e) {
									try {
										$domain = $shell->RegRead($tcpip_key . '\Interfaces\\' . $adapter . '\DhcpDomain');
									} catch (com_exception $e) { }
								}
							}
						} catch (com_exception $e) { }
					}
				}
				if (!empty($domain)) {
					self::$fqdn .= '.' . $domain;
				} 
				
			} elseif (!fCore::checkOS('windows') && !ini_get('open_basedir') && file_exists('/etc/resolv.conf')) {
				$output = file_get_contents('/etc/resolv.conf');
				if (preg_match('#^domain ([a-z0-9_.-]+)#im', $output, $match)) {
					self::$fqdn .= '.' . $match[1];
				}
			}
		}
		
		return self::$fqdn;
	}


	/**
	 * Encodes a string to UTF-8 encoded-word
	 * 
	 * @param  string  $content                   The content to encode
	 * @param  integer $first_line_prefix_length  The length of any prefix applied to the first line of the encoded word - this allows properly accounting for a header name
	 * @return string  The encoded string
	 */
	static private function makeEncodedWord($content, $first_line_prefix_length)
	{
		// Homogenize the line-endings to CRLF
		$content = str_replace("\r\n", "\n", $content);
		$content = str_replace("\r", "\n", $content);
		$content = str_replace("\n", "\r\n", $content);
		
		// Encoded word is not required if all characters are ascii
		if (!preg_match('#[\x80-\xFF]#', $content)) {
			return $content;
		}
		
		// A quick a dirty hex encoding
		$content = rawurlencode($content);
		$content = str_replace('=', '%3D', $content);
		$content = str_replace('%', '=', $content);
		
		// Decode characters that don't have to be coded
		$decodings = array(
			'=20' => '_', '=21' => '!', '=22' => '"',  '=23' => '#',
			'=24' => '$', '=25' => '%', '=26' => '&',  '=27' => "'",
			'=28' => '(', '=29' => ')', '=2A' => '*',  '=2B' => '+',
			'=2C' => ',', '=2D' => '-', '=2E' => '.',  '=2F' => '/',
			'=3A' => ':', '=3B' => ';', '=3C' => '<',  '=3E' => '>',
			'=40' => '@', '=5B' => '[', '=5C' => '\\', '=5D' => ']',
			'=5E' => '^', '=60' => '`', '=7B' => '{',  '=7C' => '|',
			'=7D' => '}', '=7E' => '~', ' '   => '_'
		);
		
		$content = strtr($content, $decodings);
		
		$length = strlen($content);
		
		$prefix = '=?utf-8?Q?';
		$suffix = '?=';
		
		$prefix_length = 10;
		$suffix_length = 2;
		
		// This loop goes through and ensures we are wrapping by 75 chars
		// including the encoded word delimiters
		$output = $prefix;
		$line_length = $prefix_length + $first_line_prefix_length;
		
		for ($i=0; $i<$length; $i++) {
			
			// Get info about the next character
			$char_length = ($content[$i] == '=') ? 3 : 1;
			$char        = $content[$i];
			if ($char_length == 3) {
				$char .= $content[$i+1] . $content[$i+2];
			}
			
			// If we have too long a line, wrap it
			if ($line_length + $suffix_length + $char_length > 75) {
				$output .= $suffix . "\r\n " . $prefix;
				$line_length = $prefix_length + 2;
			}
			
			// Add the character
			$output .= $char;
			
			// Figure out how much longer the line is
			$line_length += $char_length;
			
			// Skip characters if we have an encoded character
			$i += $char_length-1;
		}
		
		if (substr($output, -2) != $suffix) {
			$output .= $suffix;
		}
		
		return $output;
	}
	
	
	/**
	 * Resets the configuration of the class
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	static public function reset()
	{
		self::$convert_crlf   = FALSE;
		self::$fqdn           = NULL;
		self::$popen_sendmail = FALSE;
	}
	
	
	/**
	 * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
	 * 
	 * @param  mixed $value  The value to check
	 * @return boolean  If the value is string-like
	 */
	static protected function stringlike($value)
	{
		if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
			return FALSE;	
		}
		
		return TRUE;
	}
	
	
	/**
	 * Takes a block of text, unindents it and replaces {CONSTANT} tokens with the constant's value
	 * 
	 * @param string $text  The text to unindent and replace constants in
	 * @return string  The unindented text
	 */
	static public function unindentExpand($text)
	{
		$text = preg_replace('#^[ \t]*\n|\n[ \t]*$#D', '', $text);
			
		if (preg_match('#^[ \t]+(?=\S)#m', $text, $match)) {
			$text = preg_replace('#^' . preg_quote($match[0]) . '#m', '', $text);
		}
		
		preg_match_all('#\{([a-z][a-z0-9_]*)\}#i', $text, $constants, PREG_SET_ORDER);
		foreach ($constants as $constant) {
			if (!defined($constant[1])) { continue; }
			$text = preg_replace('#' . preg_quote($constant[0], '#') . '#', constant($constant[1]), $text, 1);
		}
		
		return $text;
	}
	
	
	/**
	 * The file contents to attach
	 * 
	 * @var array
	 */
	private $attachments = array();
	
	/**
	 * The email address(es) to BCC to
	 * 
	 * @var array
	 */
	private $bcc_emails = array();
	
	/**
	 * The email address to bounce to
	 * 
	 * @var string
	 */
	private $bounce_to_email = NULL;
	
	/**
	 * The email address(es) to CC to
	 * 
	 * @var array
	 */
	private $cc_emails = array();
	
	/**
	 * Custom headers
	 * 
	 * @var array
	 */
	private $custom_headers = array();
	
	/**
	 * The email address being sent from
	 * 
	 * @var string
	 */
	private $from_email = NULL;
	
	/**
	 * The HTML body of the email
	 * 
	 * @var string
	 */
	private $html_body = NULL;
	
	/**
	 * The Message-ID header for the email
	 * 
	 * @var string
	 */
	private $message_id = NULL;
	
	/**
	 * The plaintext body of the email
	 * 
	 * @var string
	 */
	private $plaintext_body = NULL;
	
	/**
	 * The recipient's S/MIME PEM certificate filename, used for encryption of the message
	 * 
	 * @var string
	 */
	private $recipients_smime_cert_file = NULL;
	
	/**
	 * The files to include as multipart/related
	 * 
	 * @var array
	 */
	private $related_files = array();
	
	/**
	 * The email address to reply to
	 * 
	 * @var string
	 */
	private $reply_to_email = NULL;
	
	/**
	 * The email address actually sending the email
	 * 
	 * @var string
	 */
	private $sender_email = NULL;
	
	/**
	 * The senders's S/MIME PEM certificate filename, used for singing the message
	 * 
	 * @var string
	 */
	private $senders_smime_cert_file = NULL;
	
	/**
	 * The senders's S/MIME private key filename, used for singing the message
	 * 
	 * @var string
	 */
	private $senders_smime_pk_file = NULL;
	
	/**
	 * The senders's S/MIME private key password, used for singing the message
	 * 
	 * @var string
	 */
	private $senders_smime_pk_password = NULL;
	
	/**
	 * If the message should be encrypted using the recipient's S/MIME certificate
	 * 
	 * @var boolean
	 */
	private $smime_encrypt = FALSE;
	
	/**
	 * If the message should be signed using the senders's S/MIME private key
	 * 
	 * @var boolean
	 */
	private $smime_sign = FALSE;
	
	/**
	 * The subject of the email
	 * 
	 * @var string
	 */
	private $subject = NULL;
	
	/**
	 * The email address(es) to send to
	 * 
	 * @var array
	 */
	private $to_emails = array();
	
	
	/**
	 * Initializes fEmail for creating message ids
	 * 
	 * @return fEmail
	 */
	public function __construct()
	{
		$this->message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
	}
	
	
	/**
	 * All requests that hit this method should be requests for callbacks
	 * 
	 * @internal
	 * 
	 * @param  string $method  The method to create a callback for
	 * @return callback  The callback for the method requested
	 */
	public function __get($method)
	{
		return array($this, $method);		
	}
	
	
	/**
	 * Adds an attachment to the email
	 * 
	 * If a duplicate filename is detected, it will be changed to be unique.
	 * 
	 * @param  string|fFile $contents   The contents of the file
	 * @param  string       $filename   The name to give the attachement - optional if `$contents` is an fFile object
	 * @param  string       $mime_type  The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function addAttachment($contents, $filename=NULL, $mime_type=NULL)
	{
		$this->extrapolateFileInfo($contents, $filename, $mime_type);
		
		while (isset($this->attachments[$filename])) {
			$filename = $this->generateNewFilename($filename);
		}
		
		$this->attachments[$filename] = array(
			'mime-type' => $mime_type,
			'contents'  => $contents
		);

		return $this;
	}
	
	
	/**
	 * Adds a “related” file to the email, returning the `Content-ID` for use in HTML
	 * 
	 * The purpose of a related file is to be able to reference it in part of
	 * the HTML body. Image `src` URLs can reference a related file by starting
	 * the URL with `cid:` and then inserting the `Content-ID`.
	 * 
	 * If a duplicate filename is detected, it will be changed to be unique.
	 * 
	 * @param  string|fFile $contents   The contents of the file
	 * @param  string       $filename   The name to give the attachement - optional if `$contents` is an fFile object
	 * @param  string       $mime_type  The mime type of the file - this allows overriding the mime type of the file if incorrectly detected
	 * @return string  The fully-formed `cid:` URL for use in HTML `src` attributes
	 */
	public function addRelatedFile($contents, $filename=NULL, $mime_type=NULL)
	{
		$this->extrapolateFileInfo($contents, $filename, $mime_type);
		
		while (isset($this->related_files[$filename])) {
			$filename = $this->generateNewFilename($filename);
		}
		
		$cid = count($this->related_files) . '.' . substr($this->message_id, 1, -1);
		
		$this->related_files[$filename] = array(
			'mime-type'  => $mime_type,
			'contents'   => $contents,
			'content-id' => '<' . $cid . '>'
		);
		
		return 'cid:' . $cid;
	}
	
	
	/**
	 * Adds a blind carbon copy (BCC) email recipient
	 * 
	 * @param  string $email  The email address to BCC
	 * @param  string $name   The recipient's name
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function addBCCRecipient($email, $name=NULL)
	{
		if (!$email) {
			return;
		}
		
		$this->bcc_emails[] = self::combineNameEmail($name, $email);

		return $this;
	}
	
	
	/**
	 * Adds a carbon copy (CC) email recipient
	 * 
	 * @param  string $email  The email address to BCC
	 * @param  string $name   The recipient's name
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function addCCRecipient($email, $name=NULL)
	{
		if (!$email) {
			return;
		}
		
		$this->cc_emails[] = self::combineNameEmail($name, $email);

		return $this;
	}
	
	
	/**
	 * Allows adding a custom header to the email
	 * 
	 * If the method is called multiple times with the same name, the last
	 * value will be used.
	 * 
	 * Please note that this class will properly format the header, including
	 * adding the `:` between the name and value and wrapping values that are
	 * too long for a single line.
	 * 
	 * @param  string $name      The name of the header
	 * @param  string $value     The value of the header
	 * @param  array  :$headers  An associative array of `{name} => {value}`
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function addCustomHeader($name, $value=NULL)
	{
		if ($value === NULL && is_array($name)) {
			foreach ($name as $key => $value) {
				$this->addCustomHeader($key, $value);
			}
			return;	
		}
		
		$lower_name = fUTF8::lower($name);
		$this->custom_headers[$lower_name] = array($name, $value);

		return $this;
	}
	
	
	/**
	 * Adds an email recipient
	 * 
	 * @param  string $email  The email address to send to
	 * @param  string $name   The recipient's name
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function addRecipient($email, $name=NULL)
	{
		if (!$email) {
			return;
		}
		
		$this->to_emails[] = self::combineNameEmail($name, $email);

		return $this;
	}
	
	
	/**
	 * Takes a multi-address email header and builds it out using an array of emails
	 * 
	 * @param  string $header  The header name without `': '`, the header is non-blank, `': '` will be added
	 * @param  array  $emails  The email addresses for the header
	 * @return string  The email header with a trailing `\r\n`
	 */
	private function buildMultiAddressHeader($header, $emails)
	{
		$header .= ': ';
		
		$first = TRUE;
		$line  = 1;
		foreach ($emails as $email) {
			if ($first) { $first = FALSE; } else { $header .= ', '; }
			
			// Try to stay within the recommended 78 character line limit
			$last_crlf_pos = (integer) strrpos($header, "\r\n");
			if (strlen($header . $email) - $last_crlf_pos > 78) {
				$header .= "\r\n ";
				$line++;
			}
			
			$header .= trim($email);
		}
		
		return $header . "\r\n";
	}
	
	
	/**
	 * Removes all To, CC and BCC recipients from the email
	 * 
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function clearRecipients()
	{
		$this->to_emails  = array();
		$this->cc_emails  = array();
		$this->bcc_emails = array();

		return $this;
	}
	
	
	/**
	 * Creates a 32-character boundary for a multipart message
	 * 
	 * @return string  A multipart boundary
	 */
	private function createBoundary()
	{
		$chars      = 'ancdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:-_';
		$last_index = strlen($chars) - 1;
		$output     = '';
		
		for ($i = 0; $i < 28; $i++) {
			$output .= $chars[rand(0, $last_index)];
		}
		return $output;
	}
	
	
	/**
	 * Builds the body of the email
	 * 
	 * @param  string $boundary  The boundary to use for the top level mime block
	 * @return string  The message body to be sent to the mail() function
	 */
	private function createBody($boundary)
	{
		$boundary_stack = array($boundary);
		
		$mime_notice = self::compose(
			"This message has been formatted using MIME. It does not appear that your\r\nemail client supports MIME."
		);
		
		$body = '';
		
		if ($this->html_body || $this->attachments) {
			$body .= $mime_notice . "\r\n\r\n";
		}
		
		if ($this->html_body && $this->related_files && $this->attachments) {
			$body    .= '--' . end($boundary_stack) . "\r\n";
			$boundary_stack[] = $this->createBoundary();
			$body .= 'Content-Type: multipart/related; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
		}
		
		if ($this->html_body && ($this->attachments || $this->related_files)) {
			$body    .= '--' . end($boundary_stack) . "\r\n";
			$boundary_stack[] = $this->createBoundary();
			$body    .= 'Content-Type: multipart/alternative; boundary="' . end($boundary_stack) . "\"\r\n\r\n";
		}
		
		if ($this->html_body || $this->attachments) {
			$body .= '--' . end($boundary_stack) . "\r\n";
			$body .= "Content-Type: text/plain; charset=utf-8\r\n";
			$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
		}
		
		$body .= $this->makeQuotedPrintable($this->plaintext_body) . "\r\n";
		
		if ($this->html_body) {
			$body .= '--' . end($boundary_stack) . "\r\n";
			$body .= "Content-Type: text/html; charset=utf-8\r\n";
			$body .= "Content-Transfer-Encoding: quoted-printable\r\n\r\n";
			$body .= $this->makeQuotedPrintable($this->html_body) . "\r\n";
		}
		
		if ($this->related_files) {
			$body .= '--' . end($boundary_stack) . "--\r\n";
			array_pop($boundary_stack);
			
			foreach ($this->related_files as $filename => $file_info) {
				$body .= '--' . end($boundary_stack) . "\r\n";
				$body .= 'Content-Type: ' . $file_info['mime-type'] . '; name="' . $filename . "\"\r\n";
				$body .= "Content-Transfer-Encoding: base64\r\n";
				$body .= 'Content-ID: ' . $file_info['content-id'] . "\r\n\r\n";
				$body .= $this->makeBase64($file_info['contents']) . "\r\n";
			}
		}
		
		if ($this->attachments) {
			
			if ($this->html_body) {
				$body .= '--' . end($boundary_stack) . "--\r\n";
				array_pop($boundary_stack);
			}
			
			foreach ($this->attachments as $filename => $file_info) {
				$body .= '--' . end($boundary_stack) . "\r\n";
				$body .= 'Content-Type: ' . $file_info['mime-type'] . "\r\n";
				$body .= "Content-Transfer-Encoding: base64\r\n";
				$body .= 'Content-Disposition: attachment; filename="' . $filename . "\";\r\n\r\n";
				$body .= $this->makeBase64($file_info['contents']) . "\r\n";
			}
		}
		
		if ($this->html_body || $this->attachments) {
			$body .= '--' . end($boundary_stack) . "--\r\n";
			array_pop($boundary_stack);
		}
		
		return $body;
	}
	
	
	/**
	 * Builds the headers for the email
	 * 
	 * @param  string $boundary    The boundary to use for the top level mime block
	 * @param  string $message_id  The message id for the message
	 * @return string  The headers to be sent to the [http://php.net/function.mail mail()] function
	 */
	private function createHeaders($boundary, $message_id)
	{
		$headers = '';
		
		if ($this->cc_emails) {
			$headers .= $this->buildMultiAddressHeader("Cc", $this->cc_emails);
		}
		
		if ($this->bcc_emails) {
			$headers .= $this->buildMultiAddressHeader("Bcc", $this->bcc_emails);
		}
		
		$headers .= "From: " . trim($this->from_email) . "\r\n";
		
		if ($this->reply_to_email) {
			$headers .= "Reply-To: " . trim($this->reply_to_email) . "\r\n";
		}
		
		if ($this->sender_email) {
			$headers .= "Sender: " . trim($this->sender_email) . "\r\n";
		}
		
		foreach ($this->custom_headers as $header_info) {
			$header_prefix = $header_info[0] . ': ';
			$headers .= $header_prefix . self::makeEncodedWord($header_info[1], strlen($header_prefix)) . "\r\n";  
		}
		
		$headers .= "Message-ID: " . $message_id . "\r\n";
		$headers .= "MIME-Version: 1.0\r\n";
		
		if (!$this->html_body && !$this->attachments) {
			$headers .= "Content-Type: text/plain; charset=utf-8\r\n";
			$headers .= "Content-Transfer-Encoding: quoted-printable\r\n";
		
		} elseif ($this->html_body && !$this->attachments) {
			if ($this->related_files) {
				$headers .= 'Content-Type: multipart/related; boundary="' . $boundary . "\"\r\n";
			} else {
				$headers .= 'Content-Type: multipart/alternative; boundary="' . $boundary . "\"\r\n";
			}
		
		} elseif ($this->attachments) {
			$headers .= 'Content-Type: multipart/mixed; boundary="' . $boundary . "\"\r\n";
		}
		
		return $headers . "\r\n";
	}
	
	
	/**
	 * Takes the body of the message and processes it with S/MIME
	 * 
	 * @param  string $to       The recipients being sent to
	 * @param  string $subject  The subject of the email
	 * @param  string $headers  The headers for the message
	 * @param  string $body     The message body
	 * @return array  `0` => The message headers, `1` => The message body
	 */
	private function createSMIMEBody($to, $subject, $headers, $body)
	{
		if (!$this->smime_encrypt && !$this->smime_sign) {
			return array($headers, $body);
		}
		
		$plaintext_file  = tempnam('', '__fEmail_');
		$ciphertext_file = tempnam('', '__fEmail_');
		
		$headers_array = array(
			'To'      => $to,
			'Subject' => $subject
		);
		
		preg_match_all('#^([\w\-]+):\s+([^\n]+\n( [^\n]+\n)*)#im', $headers, $header_matches, PREG_SET_ORDER);
		foreach ($header_matches as $header_match) {
			$headers_array[$header_match[1]] = trim($header_match[2]);
		}
		
		$body_headers = "";
		if (isset($headers_array['Content-Type'])) {
			$body_headers .= 'Content-Type: ' . $headers_array['Content-Type'] . "\r\n";
		}
		if (isset($headers_array['Content-Transfer-Encoding'])) {
			$body_headers .= 'Content-Transfer-Encoding: ' . $headers_array['Content-Transfer-Encoding'] . "\r\n";
		}
		
		if ($body_headers) {
			$body = $body_headers . "\r\n" . $body;
		}
		
		file_put_contents($plaintext_file, $body);
		file_put_contents($ciphertext_file, '');
		
		// Set up the neccessary S/MIME resources
		if ($this->smime_sign) {
			$senders_smime_cert  = file_get_contents($this->senders_smime_cert_file);
			$senders_private_key = openssl_pkey_get_private(
				file_get_contents($this->senders_smime_pk_file),
				$this->senders_smime_pk_password
			);
			
			if ($senders_private_key === FALSE) {
				throw new fValidationException(
					"The sender's S/MIME private key password specified does not appear to be valid for the private key"
				);
			}
		}
		
		if ($this->smime_encrypt) {
			$recipients_smime_cert = file_get_contents($this->recipients_smime_cert_file);
		}
		
		
		// If we are going to sign and encrypt, the best way is to sign, encrypt and then sign again
		if ($this->smime_encrypt && $this->smime_sign) {
			openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, array());
			openssl_pkcs7_encrypt($ciphertext_file, $plaintext_file, $recipients_smime_cert, array(), NULL, OPENSSL_CIPHER_RC2_128);
			openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
		
		} elseif ($this->smime_sign) {
			openssl_pkcs7_sign($plaintext_file, $ciphertext_file, $senders_smime_cert, $senders_private_key, $headers_array);
		  
		} elseif ($this->smime_encrypt) {
			openssl_pkcs7_encrypt($plaintext_file, $ciphertext_file, $recipients_smime_cert, $headers_array, NULL, OPENSSL_CIPHER_RC2_128);
		}
		
		// It seems that the contents of the ciphertext is not always \r\n line breaks
		$message = file_get_contents($ciphertext_file);
		$message = str_replace("\r\n", "\n", $message);
		$message = str_replace("\r", "\n", $message);
		$message = str_replace("\n", "\r\n", $message);
		
		list($new_headers, $new_body) = explode("\r\n\r\n", $message, 2);
		
		$new_headers = preg_replace('#^To:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
		$new_headers = preg_replace('#^Subject:[^\n]+\n( [^\n]+\n)*#mi', '', $new_headers);
		$new_headers = preg_replace("#^MIME-Version: 1.0\r?\n#mi", '', $new_headers, 1);
		$new_headers = preg_replace('#^Content-Type:\s+' . preg_quote($headers_array['Content-Type'], '#') . "\r?\n#mi", '', $new_headers);
		$new_headers = preg_replace('#^Content-Transfer-Encoding:\s+' . preg_quote($headers_array['Content-Transfer-Encoding'], '#') . "\r?\n#mi", '', $new_headers);
		
		unlink($plaintext_file);
		unlink($ciphertext_file);
		
		if ($this->smime_sign) {
			openssl_pkey_free($senders_private_key);
		}
								  
		return array($new_headers, $new_body);
	}
	
	
	/**
	 * Sets the email to be encrypted with S/MIME
	 * 
	 * @param  string $recipients_smime_cert_file  The file path to the PEM-encoded S/MIME certificate for the recipient
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function encrypt($recipients_smime_cert_file)
	{
		if (!extension_loaded('openssl')) {
			throw new fEnvironmentException(
				'S/MIME encryption was requested for an email, but the %s extension is not installed',
				'openssl'
			);
		}
		
		if (!self::stringlike($recipients_smime_cert_file)) {
			throw new fProgrammerException(
				"The recipient's S/MIME certificate filename specified, %s, does not appear to be a valid filename",
				$recipients_smime_cert_file
			);
		}
		
		$this->smime_encrypt              = TRUE;
		$this->recipients_smime_cert_file = $recipients_smime_cert_file;

		return $this;
	}
	
	
	/**
	 * Extracts just the email addresses from an array of strings containing an
	 * <email@address.com> or "Name" <email@address.com> combination.
	 * 
	 * @param array $list  The list of email or name/email to extract from
	 * @return array  The email addresses
	 */
	private function extractEmails($list)
	{
		$output = array();
		foreach ($list as $email) {
			if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
				$output[] = $match[2];
			} else {
				preg_match(self::EMAIL_REGEX, $email, $match);
				$output[] = $match[0];
			}
		}
		return $output;
	}
	
	
	/**
	 * Extracts the filename and mime-type from an fFile object
	 * 
	 * @param  string|fFile &$contents   The file to extrapolate the info from
	 * @param  string       &$filename   The filename to use for the file
	 * @param  string       &$mime_type  The mime type of the file
	 * @return void
	 */
	private function extrapolateFileInfo(&$contents, &$filename, &$mime_type)
	{
		if ($contents instanceof fFile) {
			if ($filename === NULL) {
				$filename = $contents->getName();
			}
			if ($mime_type === NULL) {
				$mime_type = $contents->getMimeType();
			}
			$contents = $contents->read();
			
		} else {
			if (!self::stringlike($filename)) {
				throw new fProgrammerException(
					'The filename specified, %s, does not appear to be a valid filename',
					$filename
				);
			}
			
			$filename = (string) $filename;
			
			if ($mime_type === NULL) {
				$mime_type = fFile::determineMimeType($filename, $contents);
			}
		}
	}
	
	
	/**
	 * Generates a new filename in an attempt to create a unique name
	 * 
	 * @param  string $filename  The filename to generate another name for
	 * @return string  The newly generated filename
	 */
	private function generateNewFilename($filename)
	{
		$filename_info = fFilesystem::getPathInfo($filename);
		if (preg_match('#_copy(\d+)($|\.)#D', $filename_info['filename'], $match)) {
			$i = $match[1] + 1;
		} else {
			$i = 1;
		}
		$extension = ($filename_info['extension']) ? '.' . $filename_info['extension'] : '';
		return preg_replace('#_copy\d+$#D', '', $filename_info['filename']) . '_copy' . $i . $extension;
	}
	
	
	/**
	 * Loads the plaintext version of the email body from a file and applies replacements
	 * 
	 * The should contain either ASCII or UTF-8 encoded text. Please see
	 * http://flourishlib.com/docs/UTF-8 for more information.
	 * 
	 * @throws fValidationException  When no file was specified, the file does not exist or the path specified is not a file
	 * 
	 * @param  string|fFile $file          The plaintext version of the email body
	 * @param  array        $replacements  The method will search the contents of the file for each key and replace it with the corresponding value
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function loadBody($file, $replacements=array())
	{
		if (!$file instanceof fFile) {
			$file = new fFile($file);	
		}
		
		$plaintext = $file->read();
		if ($replacements) {
			$plaintext = strtr($plaintext, $replacements);	
		}
		
		$this->plaintext_body = $plaintext;

		return $this;
	}
	
	
	/**
	 * Loads the plaintext version of the email body from a file and applies replacements
	 * 
	 * The should contain either ASCII or UTF-8 encoded text. Please see
	 * http://flourishlib.com/docs/UTF-8 for more information.
	 * 
	 * @throws fValidationException  When no file was specified, the file does not exist or the path specified is not a file
	 * 
	 * @param  string|fFile $file          The plaintext version of the email body
	 * @param  array        $replacements  The method will search the contents of the file for each key and replace it with the corresponding value
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function loadHTMLBody($file, $replacements=array())
	{
		if (!$file instanceof fFile) {
			$file = new fFile($file);	
		}
		
		$html = $file->read();
		if ($replacements) {
			$html = strtr($html, $replacements);	
		}
		
		$this->html_body = $html;

		return $this;
	}
	
	
	/**
	 * Encodes a string to base64
	 * 
	 * @param  string  $content  The content to encode
	 * @return string  The encoded string
	 */
	private function makeBase64($content)
	{
		return chunk_split(base64_encode($content));
	}
	
	
	/**
	 * Encodes a string to quoted-printable, properly handles UTF-8
	 * 
	 * @param  string  $content  The content to encode
	 * @return string  The encoded string
	 */
	private function makeQuotedPrintable($content)
	{
		// Homogenize the line-endings to CRLF
		$content = str_replace("\r\n", "\n", $content);
		$content = str_replace("\r", "\n", $content);
		$content = str_replace("\n", "\r\n", $content);
		
		// A quick a dirty hex encoding
		$content = rawurlencode($content);
		$content = str_replace('=', '%3D', $content);
		$content = str_replace('%', '=', $content);
		
		// Decode characters that don't have to be coded
		$decodings = array(
			'=20' => ' ', '=21' => '!', '=22' => '"', '=23' => '#',
			'=24' => '$', '=25' => '%', '=26' => '&', '=27' => "'",
			'=28' => '(', '=29' => ')', '=2A' => '*', '=2B' => '+',
			'=2C' => ',', '=2D' => '-', '=2E' => '.', '=2F' => '/',
			'=3A' => ':', '=3B' => ';', '=3C' => '<', '=3E' => '>',
			'=3F' => '?', '=40' => '@', '=5B' => '[', '=5C' => '\\',
			'=5D' => ']', '=5E' => '^', '=5F' => '_', '=60' => '`',
			'=7B' => '{', '=7C' => '|', '=7D' => '}', '=7E' => '~'
		);
		
		$content = strtr($content, $decodings);
		
		$output = '';
		
		$length = strlen($content);
		
		// This loop goes through and ensures we are wrapping by 76 chars
		$line_length = 0;
		for ($i=0; $i<$length; $i++) {
			
			// Get info about the next character
			$char_length = ($content[$i] == '=') ? 3 : 1;
			$char        = $content[$i];
			if ($char_length == 3) {
				$char .= $content[$i+1] . $content[$i+2];
			}
			
			// Skip characters if we have an encoded character, this must be
			// done before checking for whitespace at the beginning and end of
			// lines or else characters in the content will be skipped
			$i += $char_length-1;
			
			// Spaces and tabs at the beginning and ending of lines have to be encoded
			$begining_or_end = $line_length > 69 || $line_length == 0;
			$tab_or_space    = $char == ' ' || $char == "\t";
			if ($begining_or_end && $tab_or_space) {
				$char_length = 3;
				$char        = ($char == ' ') ? '=20' : '=09';
			}
			
			// If we have too long a line, wrap it
			if ($char != "\r" && $char != "\n" && $line_length + $char_length > 75) {
				$output .= "=\r\n";
				$line_length = 0;
			}
			
			// Add the character
			$output .= $char;
			
			// Figure out how much longer the line is now
			if ($char == "\r" || $char == "\n") {
				$line_length = 0;
			} else {
				$line_length += $char_length;
			}
		}
		
		return $output;
	}
	
	
	/**
	 * Sends the email
	 * 
	 * The return value is the message id, which should be included as the
	 * `Message-ID` header of the email. While almost all SMTP servers will not
	 * modify this value, testing has indicated at least one (smtp.live.com
	 * for Windows Live Mail) does.
	 * 
	 * @throws fValidationException  When ::validate() throws an exception
	 * 
	 * @param  fSMTP $connection  The SMTP connection to send the message over
	 * @return string  The message id for the message - see method description for details
	 */
	public function send($connection=NULL)
	{
		$this->validate();
		
		// The mail() function on Windows doesn't support names in headers so
		// we must strip them down to just the email address
		if ($connection === NULL && fCore::checkOS('windows')) {
			$vars = array('bcc_emails', 'bounce_to_email', 'cc_emails', 'from_email', 'reply_to_email', 'sender_email', 'to_emails');
			foreach ($vars as $var) {
				if (!is_array($this->$var)) {
					if (preg_match(self::NAME_EMAIL_REGEX, $this->$var, $match)) {
						$this->$var = $match[2];
					}
				} else {
					$new_emails = array();
					foreach ($this->$var as $email) {
						if (preg_match(self::NAME_EMAIL_REGEX, $email, $match)) {
							$email = $match[2];
						}
						$new_emails[] = $email;
					}
					$this->$var = $new_emails;
				}
			}
		}
		
		$to = substr(trim($this->buildMultiAddressHeader("To", $this->to_emails)), 4);
		
		$top_level_boundary = $this->createBoundary();
		$headers            = $this->createHeaders($top_level_boundary, $this->message_id);
		
		$subject = str_replace(array("\r", "\n"), '', $this->subject);
		$subject = self::makeEncodedWord($subject, 9);
		
		$body = $this->createBody($top_level_boundary);
		
		if ($this->smime_encrypt || $this->smime_sign) {
			list($headers, $body) = $this->createSMIMEBody($to, $subject, $headers, $body);
		}
		
		// Remove extra line breaks
		$headers = trim($headers);
		$body    = trim($body);
		
		if ($connection) {
			$to_emails = $this->extractEmails($this->to_emails);
			$to_emails = array_merge($to_emails, $this->extractEmails($this->cc_emails));
			$to_emails = array_merge($to_emails, $this->extractEmails($this->bcc_emails));
			$from = $this->bounce_to_email ? $this->bounce_to_email : current($this->extractEmails(array($this->from_email)));
			$connection->send($from, $to_emails, "To: " . $to . "\r\nSubject: " . $subject . "\r\n" . $headers, $body);
			return $this->message_id;
		}
		
		// Sendmail when not in safe mode will allow you to set the envelope from address via the -f parameter
		$parameters = NULL;
		if (!fCore::checkOS('windows') && $this->bounce_to_email) {
			preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
			$parameters = '-f ' . $matches[0];
		
		// Windows takes the Return-Path email from the sendmail_from ini setting
		} elseif (fCore::checkOS('windows') && $this->bounce_to_email) {
			$old_sendmail_from = ini_get('sendmail_from');
			preg_match(self::EMAIL_REGEX, $this->bounce_to_email, $matches);
			ini_set('sendmail_from', $matches[0]);
		}
		
		// This is a gross qmail fix that is a last resort
		if (self::$popen_sendmail || self::$convert_crlf) {
			$to      = str_replace("\r\n", "\n", $to);
			$subject = str_replace("\r\n", "\n", $subject);
			$body    = str_replace("\r\n", "\n", $body);
			$headers = str_replace("\r\n", "\n", $headers);
		}
		
		// If the user is using qmail and wants to try to fix the \r\r\n line break issue
		if (self::$popen_sendmail) {
			$sendmail_command = ini_get('sendmail_path');
			if ($parameters) {
				$sendmail_command .= ' ' . $parameters;
			}
			
			$sendmail_process = popen($sendmail_command, 'w');
			fprintf($sendmail_process, "To: %s\n", $to);
			fprintf($sendmail_process, "Subject: %s\n", $subject);
			if ($headers) {
				fprintf($sendmail_process, "%s\n", $headers);
			}
			fprintf($sendmail_process, "\n%s\n", $body);
			$error = pclose($sendmail_process);
			
		// This is the normal way to send mail
		} else {
			// On Windows, mail() sends directly to an SMTP server and will
			// strip a leading . from the body
			if (fCore::checkOS('windows')) {
				$body = preg_replace('#^\.#', '..', $body);
			}
			
			if ($parameters) {
				$error = !mail($to, $subject, $body, $headers, $parameters);
			} else {
				$error = !mail($to, $subject, $body, $headers);
			}
		}
		
		if (fCore::checkOS('windows') && $this->bounce_to_email) {
			ini_set('sendmail_from', $old_sendmail_from);
		}
		
		if ($error) {
			throw new fConnectivityException(
				'An error occured while trying to send the email entitled %s',
				$this->subject
			);
		}
		
		return $this->message_id;
	}
	
	
	/**
	 * Sets the plaintext version of the email body
	 * 
	 * This method accepts either ASCII or UTF-8 encoded text. Please see
	 * http://flourishlib.com/docs/UTF-8 for more information.
	 * 
	 * @param  string  $plaintext                  The plaintext version of the email body
	 * @param  boolean $unindent_expand_constants  If this is `TRUE`, the body will be unindented as much as possible and {CONSTANT_NAME} will be replaced with the value of the constant
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setBody($plaintext, $unindent_expand_constants=FALSE)
	{
		if ($unindent_expand_constants) {
			$plaintext = self::unindentExpand($plaintext);
		}
		
		$this->plaintext_body = $plaintext;

		return $this;
	}
	
	
	/**
	 * Adds the email address the email will be bounced to
	 * 
	 * This email address will be set to the `Return-Path` header.
	 * 
	 * @param  string $email  The email address to bounce to
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setBounceToEmail($email)
	{
		if (ini_get('safe_mode') && !fCore::checkOS('windows')) {
			throw new fProgrammerException('It is not possible to set a Bounce-To Email address when safe mode is enabled on a non-Windows server');
		}
		if (!$email) {
			return;
		}
		
		$this->bounce_to_email = self::combineNameEmail('', $email);

		return $this;
	}
	
	
	/**
	 * Adds the `From:` email address to the email
	 * 
	 * @param  string $email  The email address being sent from
	 * @param  string $name   The from email user's name - unfortunately on windows this is ignored
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setFromEmail($email, $name=NULL)
	{
		if (!$email) {
			return;
		}
		
		$this->from_email = self::combineNameEmail($name, $email);

		return $this;
	}
	
	
	/**
	 * Sets the HTML version of the email body
	 * 
	 * This method accepts either ASCII or UTF-8 encoded text. Please see
	 * http://flourishlib.com/docs/UTF-8 for more information.
	 * 
	 * @param  string $html  The HTML version of the email body
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setHTMLBody($html)
	{
		$this->html_body = $html;

		return $this;
	}
	
	
	/**
	 * Adds the `Reply-To:` email address to the email
	 * 
	 * @param  string $email  The email address to reply to
	 * @param  string $name   The reply-to email user's name
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setReplyToEmail($email, $name=NULL)
	{
		if (!$email) {
			return;
		}
		
		$this->reply_to_email = self::combineNameEmail($name, $email);

		return $this;
	}
	
	
	/**
	 * Adds the `Sender:` email address to the email
	 * 
	 * The `Sender:` header is used to indicate someone other than the `From:`
	 * address is actually submitting the message to the network.
	 * 
	 * @param  string $email  The email address the message is actually being sent from
	 * @param  string $name   The sender email user's name
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setSenderEmail($email, $name=NULL)
	{
		if (!$email) {
			return;
		}
		
		$this->sender_email = self::combineNameEmail($name, $email);

		return $this;
	}
	
	
	/**
	 * Sets the subject of the email
	 * 
	 * This method accepts either ASCII or UTF-8 encoded text. Please see
	 * http://flourishlib.com/docs/UTF-8 for more information.
	 * 
	 * @param  string $subject  The subject of the email
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function setSubject($subject)
	{
		$this->subject = $subject;

		return $this;
	}
	
	
	/**
	 * Sets the email to be signed with S/MIME
	 * 
	 * @param  string $senders_smime_cert_file    The file path to the sender's PEM-encoded S/MIME certificate
	 * @param  string $senders_smime_pk_file      The file path to the sender's S/MIME private key
	 * @param  string $senders_smime_pk_password  The password for the sender's S/MIME private key
	 * @return fEmail  The email object, to allow for method chaining
	 */
	public function sign($senders_smime_cert_file, $senders_smime_pk_file, $senders_smime_pk_password)
	{
		if (!extension_loaded('openssl')) {
			throw new fEnvironmentException(
				'An S/MIME signature was requested for an email, but the %s extension is not installed',
				'openssl'
			);
		}
		
		if (!self::stringlike($senders_smime_cert_file)) {
			throw new fProgrammerException(
				"The sender's S/MIME certificate file specified, %s, does not appear to be a valid filename",
				$senders_smime_cert_file
			);
		}
		if (!file_exists($senders_smime_cert_file) || !is_readable($senders_smime_cert_file)) {
			throw new fEnvironmentException(
				"The sender's S/MIME certificate file specified, %s, does not exist or could not be read",
				$senders_smime_cert_file
			);
		}
		
		if (!self::stringlike($senders_smime_pk_file)) {
			throw new fProgrammerException(
				"The sender's S/MIME primary key file specified, %s, does not appear to be a valid filename",
				$senders_smime_pk_file
			);
		}
		if (!file_exists($senders_smime_pk_file) || !is_readable($senders_smime_pk_file)) {
			throw new fEnvironmentException(
				"The sender's S/MIME primary key file specified, %s, does not exist or could not be read",
				$senders_smime_pk_file
			);
		}
		
		$this->smime_sign                = TRUE;
		$this->senders_smime_cert_file   = $senders_smime_cert_file;
		$this->senders_smime_pk_file     = $senders_smime_pk_file;
		$this->senders_smime_pk_password = $senders_smime_pk_password;

		return $this;
	}
	
	
	/**
	 * Validates that all of the parts of the email are valid
	 * 
	 * @throws fValidationException  When part of the email is missing or formatted incorrectly
	 * 
	 * @return void
	 */
	private function validate()
	{
		$validation_messages = array();
		
		// Check all multi-address email field
		$multi_address_field_list = array(
			'to_emails'  => self::compose('recipient'),
			'cc_emails'  => self::compose('CC recipient'),
			'bcc_emails' => self::compose('BCC recipient')
		);
		
		foreach ($multi_address_field_list as $field => $name) {
			foreach ($this->$field as $email) {
				if ($email && !preg_match(self::NAME_EMAIL_REGEX, $email) && !preg_match(self::EMAIL_REGEX, $email)) {
					$validation_messages[] = htmlspecialchars(self::compose(
						'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
						$name,
						$email
					), ENT_QUOTES, 'UTF-8');
				}
			}
		}
		
		// Check all single-address email fields
		$single_address_field_list = array(
			'from_email'      => self::compose('From email address'),
			'reply_to_email'  => self::compose('Reply-To email address'),
			'sender_email'    => self::compose('Sender email address'),
			'bounce_to_email' => self::compose('Bounce-To email address')
		);
		
		foreach ($single_address_field_list as $field => $name) {
			if ($this->$field && !preg_match(self::NAME_EMAIL_REGEX, $this->$field) && !preg_match(self::EMAIL_REGEX, $this->$field)) {
				$validation_messages[] = htmlspecialchars(self::compose(
					'The %1$s %2$s is not a valid email address. Should be like "John Smith" <name@example.com> or name@example.com.',
					$name,
					$this->$field
				), ENT_QUOTES, 'UTF-8');
			}
		}
		
		// Make sure the required fields are all set
		if (!$this->to_emails) {
			$validation_messages[] = self::compose(
				"Please provide at least one recipient"
			);
		}
		
		if (!$this->from_email) {
			$validation_messages[] = self::compose(
				"Please provide the from email address"
			);
		}
		
		if (!self::stringlike($this->subject)) {
			$validation_messages[] = self::compose(
				"Please provide an email subject"
			);
		}
		
		if (strpos($this->subject, "\n") !== FALSE) {
			$validation_messages[] = self::compose(
				"The subject contains one or more newline characters"
			);	
		}
		
		if (!self::stringlike($this->plaintext_body)) {
			$validation_messages[] = self::compose(
				"Please provide a plaintext email body"
			);
		}
		
		// Make sure the attachments look good
		foreach ($this->attachments as $filename => $file_info) {
			if (!self::stringlike($file_info['mime-type'])) {
				$validation_messages[] = self::compose(
					"No mime-type was specified for the attachment %s",
					$filename
				);
			}
			if (!self::stringlike($file_info['contents'])) {
				$validation_messages[] = self::compose(
					"The attachment %s appears to be a blank file",
					$filename
				);
			}
		}
		
		if ($validation_messages) {
			throw new fValidationException(
				'The email could not be sent because:',
				$validation_messages
			);	
		}
	}
}



/**
 * Copyright (c) 2008-2011 Will Bond <will@flourishlib.com>, others
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fEmptySetException.php.



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
/**
 * An exception when an fRecordSet does not contain any elements
 * 
 * @copyright  Copyright (c) 2007-2008 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fEmptySetException
 * 
 * @version    1.0.0b
 * @changes    1.0.0b  The initial implementation [wb, 2007-06-14]
 */
class fEmptySetException extends fExpectedException
{
}



/**
 * Copyright (c) 2007-2008 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fEnvironmentException.php.



















































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
/**
 * An exception caused by an environment error such a file permissions
 * 
 * @copyright  Copyright (c) 2007-2008 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fEnvironmentException
 * 
 * @version    1.0.0b
 * @changes    1.0.0b  The initial implementation [wb, 2007-06-14]
 */
class fEnvironmentException extends fUnexpectedException
{
}



/**
 * Copyright (c) 2007-2008 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

Added lib/fException.php.



































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>