1<?php
2/**
3 * CodeIgniter
4 *
5 * An open source application development framework for PHP
6 *
7 * This content is released under the MIT License (MIT)
8 *
9 * Copyright (c) 2014 - 2018, British Columbia Institute of Technology
10 *
11 * Permission is hereby granted, free of charge, to any person obtaining a copy
12 * of this software and associated documentation files (the "Software"), to deal
13 * in the Software without restriction, including without limitation the rights
14 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 * copies of the Software, and to permit persons to whom the Software is
16 * furnished to do so, subject to the following conditions:
17 *
18 * The above copyright notice and this permission notice shall be included in
19 * all copies or substantial portions of the Software.
20 *
21 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 * THE SOFTWARE.
28 *
29 * @package	CodeIgniter
30 * @author	EllisLab Dev Team
31 * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
32 * @copyright	Copyright (c) 2014 - 2018, British Columbia Institute of Technology (http://bcit.ca/)
33 * @license	http://opensource.org/licenses/MIT	MIT License
34 * @link	https://codeigniter.com
35 * @since	Version 1.0.0
36 * @filesource
37 */
38defined('BASEPATH') OR exit('No direct script access allowed');
39
40/**
41 * Trackback Class
42 *
43 * Trackback Sending/Receiving Class
44 *
45 * @package		CodeIgniter
46 * @subpackage	Libraries
47 * @category	Trackbacks
48 * @author		EllisLab Dev Team
49 * @link		https://codeigniter.com/user_guide/libraries/trackback.html
50 */
51class CI_Trackback {
52
53	/**
54	 * Character set
55	 *
56	 * @var	string
57	 */
58	public $charset = 'UTF-8';
59
60	/**
61	 * Trackback data
62	 *
63	 * @var	array
64	 */
65	public $data = array(
66		'url' => '',
67		'title' => '',
68		'excerpt' => '',
69		'blog_name' => '',
70		'charset' => ''
71	);
72
73	/**
74	 * Convert ASCII flag
75	 *
76	 * Whether to convert high-ASCII and MS Word
77	 * characters to HTML entities.
78	 *
79	 * @var	bool
80	 */
81	public $convert_ascii = TRUE;
82
83	/**
84	 * Response
85	 *
86	 * @var	string
87	 */
88	public $response = '';
89
90	/**
91	 * Error messages list
92	 *
93	 * @var	string[]
94	 */
95	public $error_msg = array();
96
97	// --------------------------------------------------------------------
98
99	/**
100	 * Constructor
101	 *
102	 * @return	void
103	 */
104	public function __construct()
105	{
106		log_message('info', 'Trackback Class Initialized');
107	}
108
109	// --------------------------------------------------------------------
110
111	/**
112	 * Send Trackback
113	 *
114	 * @param	array
115	 * @return	bool
116	 */
117	public function send($tb_data)
118	{
119		if ( ! is_array($tb_data))
120		{
121			$this->set_error('The send() method must be passed an array');
122			return FALSE;
123		}
124
125		// Pre-process the Trackback Data
126		foreach (array('url', 'title', 'excerpt', 'blog_name', 'ping_url') as $item)
127		{
128			if ( ! isset($tb_data[$item]))
129			{
130				$this->set_error('Required item missing: '.$item);
131				return FALSE;
132			}
133
134			switch ($item)
135			{
136				case 'ping_url':
137					$$item = $this->extract_urls($tb_data[$item]);
138					break;
139				case 'excerpt':
140					$$item = $this->limit_characters($this->convert_xml(strip_tags(stripslashes($tb_data[$item]))));
141					break;
142				case 'url':
143					$$item = str_replace('&#45;', '-', $this->convert_xml(strip_tags(stripslashes($tb_data[$item]))));
144					break;
145				default:
146					$$item = $this->convert_xml(strip_tags(stripslashes($tb_data[$item])));
147					break;
148			}
149
150			// Convert High ASCII Characters
151			if ($this->convert_ascii === TRUE && in_array($item, array('excerpt', 'title', 'blog_name'), TRUE))
152			{
153				$$item = $this->convert_ascii($$item);
154			}
155		}
156
157		// Build the Trackback data string
158		$charset = isset($tb_data['charset']) ? $tb_data['charset'] : $this->charset;
159
160		$data = 'url='.rawurlencode($url).'&title='.rawurlencode($title).'&blog_name='.rawurlencode($blog_name)
161			.'&excerpt='.rawurlencode($excerpt).'&charset='.rawurlencode($charset);
162
163		// Send Trackback(s)
164		$return = TRUE;
165		if (count($ping_url) > 0)
166		{
167			foreach ($ping_url as $url)
168			{
169				if ($this->process($url, $data) === FALSE)
170				{
171					$return = FALSE;
172				}
173			}
174		}
175
176		return $return;
177	}
178
179	// --------------------------------------------------------------------
180
181	/**
182	 * Receive Trackback  Data
183	 *
184	 * This function simply validates the incoming TB data.
185	 * It returns FALSE on failure and TRUE on success.
186	 * If the data is valid it is set to the $this->data array
187	 * so that it can be inserted into a database.
188	 *
189	 * @return	bool
190	 */
191	public function receive()
192	{
193		foreach (array('url', 'title', 'blog_name', 'excerpt') as $val)
194		{
195			if (empty($_POST[$val]))
196			{
197				$this->set_error('The following required POST variable is missing: '.$val);
198				return FALSE;
199			}
200
201			$this->data['charset'] = isset($_POST['charset']) ? strtoupper(trim($_POST['charset'])) : 'auto';
202
203			if ($val !== 'url' && MB_ENABLED === TRUE)
204			{
205				if (MB_ENABLED === TRUE)
206				{
207					$_POST[$val] = mb_convert_encoding($_POST[$val], $this->charset, $this->data['charset']);
208				}
209				elseif (ICONV_ENABLED === TRUE)
210				{
211					$_POST[$val] = @iconv($this->data['charset'], $this->charset.'//IGNORE', $_POST[$val]);
212				}
213			}
214
215			$_POST[$val] = ($val !== 'url') ? $this->convert_xml(strip_tags($_POST[$val])) : strip_tags($_POST[$val]);
216
217			if ($val === 'excerpt')
218			{
219				$_POST['excerpt'] = $this->limit_characters($_POST['excerpt']);
220			}
221
222			$this->data[$val] = $_POST[$val];
223		}
224
225		return TRUE;
226	}
227
228	// --------------------------------------------------------------------
229
230	/**
231	 * Send Trackback Error Message
232	 *
233	 * Allows custom errors to be set. By default it
234	 * sends the "incomplete information" error, as that's
235	 * the most common one.
236	 *
237	 * @param	string
238	 * @return	void
239	 */
240	public function send_error($message = 'Incomplete Information')
241	{
242		exit('<?xml version="1.0" encoding="utf-8"?'.">\n<response>\n<error>1</error>\n<message>".$message."</message>\n</response>");
243	}
244
245	// --------------------------------------------------------------------
246
247	/**
248	 * Send Trackback Success Message
249	 *
250	 * This should be called when a trackback has been
251	 * successfully received and inserted.
252	 *
253	 * @return	void
254	 */
255	public function send_success()
256	{
257		exit('<?xml version="1.0" encoding="utf-8"?'.">\n<response>\n<error>0</error>\n</response>");
258	}
259
260	// --------------------------------------------------------------------
261
262	/**
263	 * Fetch a particular item
264	 *
265	 * @param	string
266	 * @return	string
267	 */
268	public function data($item)
269	{
270		return isset($this->data[$item]) ? $this->data[$item] : '';
271	}
272
273	// --------------------------------------------------------------------
274
275	/**
276	 * Process Trackback
277	 *
278	 * Opens a socket connection and passes the data to
279	 * the server. Returns TRUE on success, FALSE on failure
280	 *
281	 * @param	string
282	 * @param	string
283	 * @return	bool
284	 */
285	public function process($url, $data)
286	{
287		$target = parse_url($url);
288
289		// Open the socket
290		if ( ! $fp = @fsockopen($target['host'], 80))
291		{
292			$this->set_error('Invalid Connection: '.$url);
293			return FALSE;
294		}
295
296		// Build the path
297		$path = isset($target['path']) ? $target['path'] : $url;
298		empty($target['query']) OR $path .= '?'.$target['query'];
299
300		// Add the Trackback ID to the data string
301		if ($id = $this->get_id($url))
302		{
303			$data = 'tb_id='.$id.'&'.$data;
304		}
305
306		// Transfer the data
307		fputs($fp, 'POST '.$path." HTTP/1.0\r\n");
308		fputs($fp, 'Host: '.$target['host']."\r\n");
309		fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
310		fputs($fp, 'Content-length: '.strlen($data)."\r\n");
311		fputs($fp, "Connection: close\r\n\r\n");
312		fputs($fp, $data);
313
314		// Was it successful?
315
316		$this->response = '';
317		while ( ! feof($fp))
318		{
319			$this->response .= fgets($fp, 128);
320		}
321		@fclose($fp);
322
323		if (stripos($this->response, '<error>0</error>') === FALSE)
324		{
325			$message = preg_match('/<message>(.*?)<\/message>/is', $this->response, $match)
326				? trim($match[1])
327				: 'An unknown error was encountered';
328			$this->set_error($message);
329			return FALSE;
330		}
331
332		return TRUE;
333	}
334
335	// --------------------------------------------------------------------
336
337	/**
338	 * Extract Trackback URLs
339	 *
340	 * This function lets multiple trackbacks be sent.
341	 * It takes a string of URLs (separated by comma or
342	 * space) and puts each URL into an array
343	 *
344	 * @param	string
345	 * @return	string
346	 */
347	public function extract_urls($urls)
348	{
349		// Remove the pesky white space and replace with a comma, then replace doubles.
350		$urls = str_replace(',,', ',', preg_replace('/\s*(\S+)\s*/', '\\1,', $urls));
351
352		// Break into an array via commas and remove duplicates
353		$urls = array_unique(preg_split('/[,]/', rtrim($urls, ',')));
354
355		array_walk($urls, array($this, 'validate_url'));
356		return $urls;
357	}
358
359	// --------------------------------------------------------------------
360
361	/**
362	 * Validate URL
363	 *
364	 * Simply adds "http://" if missing
365	 *
366	 * @param	string
367	 * @return	void
368	 */
369	public function validate_url(&$url)
370	{
371		$url = trim($url);
372
373		if (stripos($url, 'http') !== 0)
374		{
375			$url = 'http://'.$url;
376		}
377	}
378
379	// --------------------------------------------------------------------
380
381	/**
382	 * Find the Trackback URL's ID
383	 *
384	 * @param	string
385	 * @return	string
386	 */
387	public function get_id($url)
388	{
389		$tb_id = '';
390
391		if (strpos($url, '?') !== FALSE)
392		{
393			$tb_array = explode('/', $url);
394			$tb_end   = $tb_array[count($tb_array)-1];
395
396			if ( ! is_numeric($tb_end))
397			{
398				$tb_end  = $tb_array[count($tb_array)-2];
399			}
400
401			$tb_array = explode('=', $tb_end);
402			$tb_id	= $tb_array[count($tb_array)-1];
403		}
404		else
405		{
406			$url = rtrim($url, '/');
407
408			$tb_array = explode('/', $url);
409			$tb_id	= $tb_array[count($tb_array)-1];
410
411			if ( ! is_numeric($tb_id))
412			{
413				$tb_id = $tb_array[count($tb_array)-2];
414			}
415		}
416
417		return ctype_digit((string) $tb_id) ? $tb_id : FALSE;
418	}
419
420	// --------------------------------------------------------------------
421
422	/**
423	 * Convert Reserved XML characters to Entities
424	 *
425	 * @param	string
426	 * @return	string
427	 */
428	public function convert_xml($str)
429	{
430		$temp = '__TEMP_AMPERSANDS__';
431
432		$str = preg_replace(array('/&#(\d+);/', '/&(\w+);/'), $temp.'\\1;', $str);
433
434		$str = str_replace(array('&', '<', '>', '"', "'", '-'),
435					array('&amp;', '&lt;', '&gt;', '&quot;', '&#39;', '&#45;'),
436					$str);
437
438		return preg_replace(array('/'.$temp.'(\d+);/', '/'.$temp.'(\w+);/'), array('&#\\1;', '&\\1;'), $str);
439	}
440
441	// --------------------------------------------------------------------
442
443	/**
444	 * Character limiter
445	 *
446	 * Limits the string based on the character count. Will preserve complete words.
447	 *
448	 * @param	string
449	 * @param	int
450	 * @param	string
451	 * @return	string
452	 */
453	public function limit_characters($str, $n = 500, $end_char = '&#8230;')
454	{
455		if (strlen($str) < $n)
456		{
457			return $str;
458		}
459
460		$str = preg_replace('/\s+/', ' ', str_replace(array("\r\n", "\r", "\n"), ' ', $str));
461
462		if (strlen($str) <= $n)
463		{
464			return $str;
465		}
466
467		$out = '';
468		foreach (explode(' ', trim($str)) as $val)
469		{
470			$out .= $val.' ';
471			if (strlen($out) >= $n)
472			{
473				return rtrim($out).$end_char;
474			}
475		}
476	}
477
478	// --------------------------------------------------------------------
479
480	/**
481	 * High ASCII to Entities
482	 *
483	 * Converts Hight ascii text and MS Word special chars
484	 * to character entities
485	 *
486	 * @param	string
487	 * @return	string
488	 */
489	public function convert_ascii($str)
490	{
491		$count	= 1;
492		$out	= '';
493		$temp	= array();
494
495		for ($i = 0, $s = strlen($str); $i < $s; $i++)
496		{
497			$ordinal = ord($str[$i]);
498
499			if ($ordinal < 128)
500			{
501				$out .= $str[$i];
502			}
503			else
504			{
505				if (count($temp) === 0)
506				{
507					$count = ($ordinal < 224) ? 2 : 3;
508				}
509
510				$temp[] = $ordinal;
511
512				if (count($temp) === $count)
513				{
514					$number = ($count === 3)
515						? (($temp[0] % 16) * 4096) + (($temp[1] % 64) * 64) + ($temp[2] % 64)
516						: (($temp[0] % 32) * 64) + ($temp[1] % 64);
517
518					$out .= '&#'.$number.';';
519					$count = 1;
520					$temp = array();
521				}
522			}
523		}
524
525		return $out;
526	}
527
528	// --------------------------------------------------------------------
529
530	/**
531	 * Set error message
532	 *
533	 * @param	string
534	 * @return	void
535	 */
536	public function set_error($msg)
537	{
538		log_message('error', $msg);
539		$this->error_msg[] = $msg;
540	}
541
542	// --------------------------------------------------------------------
543
544	/**
545	 * Show error messages
546	 *
547	 * @param	string
548	 * @param	string
549	 * @return	string
550	 */
551	public function display_errors($open = '<p>', $close = '</p>')
552	{
553		return (count($this->error_msg) > 0) ? $open.implode($close.$open, $this->error_msg).$close : '';
554	}
555
556}
557