Completed
Pull Request — master (#2278)
by ྅༻ Ǭɀħ
14:42
created

includes/functions.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * YOURLS
4
 * Function library
5
 */
6
7
/**
8
 * Determine the allowed character set in short URLs
9
 *
10
 */
11
function yourls_get_shorturl_charset() {
12
	static $charset = null;
13
	if( $charset !== null )
14
		return $charset;
15
16
    if( defined('YOURLS_URL_CONVERT') && in_array( YOURLS_URL_CONVERT, array( 62, 64 ) ) ) {
17
        $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
18
    } else {
19
        // defined to 36, or wrongly defined
20
        $charset = '0123456789abcdefghijklmnopqrstuvwxyz';
21
    }
22
23
	$charset = yourls_apply_filter( 'get_shorturl_charset', $charset );
24
	return $charset;
25
}
26
27
/**
28
 * Make an optimized regexp pattern from a string of characters
29
 *
30
 */
31
function yourls_make_regexp_pattern( $string ) {
32
	$pattern = preg_quote( $string, '@' ); // add @ as an escaped character because @ is used as the regexp delimiter in yourls-loader.php
33
	// Simple benchmarks show that regexp with smarter sequences (0-9, a-z, A-Z...) are not faster or slower than 0123456789 etc...
34
	return $pattern;
35
}
36
37
/**
38
 * Is a URL a short URL? Accept either 'http://sho.rt/abc' or 'abc'
39
 *
40
 */
41
function yourls_is_shorturl( $shorturl ) {
42
	// TODO: make sure this function evolves with the feature set.
43
44
	$is_short = false;
45
46
	// Is $shorturl a URL (http://sho.rt/abc) or a keyword (abc) ?
47
	if( yourls_get_protocol( $shorturl ) ) {
48
		$keyword = yourls_get_relative_url( $shorturl );
49
	} else {
50
		$keyword = $shorturl;
51
	}
52
53
	// Check if it's a valid && used keyword
54
	if( $keyword && $keyword == yourls_sanitize_string( $keyword ) && yourls_keyword_is_taken( $keyword ) ) {
55
		$is_short = true;
56
	}
57
58
	return yourls_apply_filter( 'is_shorturl', $is_short, $shorturl );
59
}
60
61
/**
62
 * Check to see if a given keyword is reserved (ie reserved URL or an existing page). Returns bool
63
 *
64
 */
65
function yourls_keyword_is_reserved( $keyword ) {
66
	global $yourls_reserved_URL;
67
	$keyword = yourls_sanitize_keyword( $keyword );
68
	$reserved = false;
69
70
	if ( in_array( $keyword, $yourls_reserved_URL)
71
		or file_exists( YOURLS_ABSPATH ."/pages/$keyword.php" )
72
		or is_dir( YOURLS_ABSPATH ."/$keyword" )
73
	)
74
		$reserved = true;
75
76
	return yourls_apply_filter( 'keyword_is_reserved', $reserved, $keyword );
77
}
78
79
/**
80
 * Function: Get client IP Address. Returns a DB safe string.
81
 *
82
 */
83
function yourls_get_IP() {
84
	$ip = '';
85
86
	// Precedence: if set, X-Forwarded-For > HTTP_X_FORWARDED_FOR > HTTP_CLIENT_IP > HTTP_VIA > REMOTE_ADDR
87
	$headers = array( 'X-Forwarded-For', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_VIA', 'REMOTE_ADDR' );
88
	foreach( $headers as $header ) {
89
		if ( !empty( $_SERVER[ $header ] ) ) {
90
			$ip = $_SERVER[ $header ];
91
			break;
92
		}
93
	}
94
95
	// headers can contain multiple IPs (X-Forwarded-For = client, proxy1, proxy2). Take first one.
96
	if ( strpos( $ip, ',' ) !== false )
97
		$ip = substr( $ip, 0, strpos( $ip, ',' ) );
98
99
	return yourls_apply_filter( 'get_IP', yourls_sanitize_ip( $ip ) );
100
}
101
102
/**
103
 * Get next id a new link will have if no custom keyword provided
104
 *
105
 */
106
function yourls_get_next_decimal() {
107
	return yourls_apply_filter( 'get_next_decimal', (int)yourls_get_option( 'next_id' ) );
108
}
109
110
/**
111
 * Update id for next link with no custom keyword
112
 *
113
 */
114
function yourls_update_next_decimal( $int = '' ) {
115
	$int = ( $int == '' ) ? yourls_get_next_decimal() + 1 : (int)$int ;
116
	$update = yourls_update_option( 'next_id', $int );
117
	yourls_do_action( 'update_next_decimal', $int, $update );
118
	return $update;
119
}
120
121
/**
122
 * Delete a link in the DB
123
 *
124
 */
125
function yourls_delete_link_by_keyword( $keyword ) {
126
	// Allow plugins to short-circuit the whole function
127
	$pre = yourls_apply_filter( 'shunt_delete_link_by_keyword', null, $keyword );
128
	if ( null !== $pre )
129
		return $pre;
130
131
	global $ydb;
132
133
	$table = YOURLS_DB_TABLE_URL;
134
	$keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
135
	$delete = $ydb->query("DELETE FROM `$table` WHERE `keyword` = '$keyword';");
136
	yourls_do_action( 'delete_link', $keyword, $delete );
137
	return $delete;
138
}
139
140
/**
141
 * SQL query to insert a new link in the DB. Returns boolean for success or failure of the inserting
142
 *
143
 */
144
function yourls_insert_link_in_db( $url, $keyword, $title = '' ) {
145
	global $ydb;
146
147
	$url     = yourls_escape( yourls_sanitize_url( $url ) );
148
	$keyword = yourls_escape( yourls_sanitize_keyword( $keyword ) );
149
	$title   = yourls_escape( yourls_sanitize_title( $title ) );
150
151
	$table = YOURLS_DB_TABLE_URL;
152
	$timestamp = date('Y-m-d H:i:s');
153
	$ip = yourls_get_IP();
154
	$insert = $ydb->query("INSERT INTO `$table` (`keyword`, `url`, `title`, `timestamp`, `ip`, `clicks`) VALUES('$keyword', '$url', '$title', '$timestamp', '$ip', 0);");
155
156
	yourls_do_action( 'insert_link', (bool)$insert, $url, $keyword, $title, $timestamp, $ip );
157
158
	return (bool)$insert;
159
}
160
161
/**
162
 * Check if a URL already exists in the DB. Return NULL (doesn't exist) or an object with URL informations.
163
 *
164
 */
165
function yourls_url_exists( $url ) {
166
	// Allow plugins to short-circuit the whole function
167
	$pre = yourls_apply_filter( 'shunt_url_exists', false, $url );
168
	if ( false !== $pre )
169
		return $pre;
170
171
	global $ydb;
172
	$table = YOURLS_DB_TABLE_URL;
173
	$url   = yourls_escape( yourls_sanitize_url( $url) );
174
	$url_exists = $ydb->get_row( "SELECT * FROM `$table` WHERE `url` = '".$url."';" );
175
176
	return yourls_apply_filter( 'url_exists', $url_exists, $url );
177
}
178
179
/**
180
 * Add a new link in the DB, either with custom keyword, or find one
181
 *
182
 */
183
function yourls_add_new_link( $url, $keyword = '', $title = '' ) {
184
	// Allow plugins to short-circuit the whole function
185
	$pre = yourls_apply_filter( 'shunt_add_new_link', false, $url, $keyword, $title );
186
	if ( false !== $pre )
187
		return $pre;
188
189
	$url = yourls_encodeURI( $url );
190
	$url = yourls_escape( yourls_sanitize_url( $url ) );
191
	if ( !$url || $url == 'http://' || $url == 'https://' ) {
192
		$return['status']    = 'fail';
193
		$return['code']      = 'error:nourl';
194
		$return['message']   = yourls__( 'Missing or malformed URL' );
195
		$return['errorCode'] = '400';
196
		return yourls_apply_filter( 'add_new_link_fail_nourl', $return, $url, $keyword, $title );
197
	}
198
199
	// Prevent DB flood
200
	$ip = yourls_get_IP();
201
	yourls_check_IP_flood( $ip );
202
203
	// Prevent internal redirection loops: cannot shorten a shortened URL
204
	if( yourls_get_relative_url( $url ) ) {
205
		if( yourls_is_shorturl( $url ) ) {
206
			$return['status']    = 'fail';
207
			$return['code']      = 'error:noloop';
208
			$return['message']   = yourls__( 'URL is a short URL' );
209
			$return['errorCode'] = '400';
210
			return yourls_apply_filter( 'add_new_link_fail_noloop', $return, $url, $keyword, $title );
211
		}
212
	}
213
214
	yourls_do_action( 'pre_add_new_link', $url, $keyword, $title );
215
216
	$strip_url = stripslashes( $url );
217
	$return = array();
218
219
	// duplicates allowed or new URL => store it
220
	if( yourls_allow_duplicate_longurls() || !( $url_exists = yourls_url_exists( $url ) ) ) {
221
222
		if( isset( $title ) && !empty( $title ) ) {
223
			$title = yourls_sanitize_title( $title );
224
		} else {
225
			$title = yourls_get_remote_title( $url );
226
		}
227
		$title = yourls_apply_filter( 'add_new_title', $title, $url, $keyword );
228
229
		// Custom keyword provided
230
		if ( $keyword ) {
231
232
			yourls_do_action( 'add_new_link_custom_keyword', $url, $keyword, $title );
233
234
			$keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
235
			$keyword = yourls_apply_filter( 'custom_keyword', $keyword, $url, $title );
236 View Code Duplication
			if ( !yourls_keyword_is_free( $keyword ) ) {
237
				// This shorturl either reserved or taken already
238
				$return['status']  = 'fail';
239
				$return['code']    = 'error:keyword';
240
				$return['message'] = yourls_s( 'Short URL %s already exists in database or is reserved', $keyword );
241
			} else {
242
				// all clear, store !
243
				yourls_insert_link_in_db( $url, $keyword, $title );
244
				$return['url']      = array('keyword' => $keyword, 'url' => $strip_url, 'title' => $title, 'date' => date('Y-m-d H:i:s'), 'ip' => $ip );
245
				$return['status']   = 'success';
246
				$return['message']  = /* //translators: eg "http://someurl/ added to DB" */ yourls_s( '%s added to database', yourls_trim_long_string( $strip_url ) );
247
				$return['title']    = $title;
248
				$return['html']     = yourls_table_add_row( $keyword, $url, $title, $ip, 0, time() );
249
				$return['shorturl'] = YOURLS_SITE .'/'. $keyword;
250
			}
251
252
		// Create random keyword
253
		} else {
254
255
			yourls_do_action( 'add_new_link_create_keyword', $url, $keyword, $title );
256
257
			$timestamp = date( 'Y-m-d H:i:s' );
258
			$id = yourls_get_next_decimal();
259
			$ok = false;
260
			do {
261
				$keyword = yourls_int2string( $id );
262
				$keyword = yourls_apply_filter( 'random_keyword', $keyword, $url, $title );
263 View Code Duplication
				if ( yourls_keyword_is_free($keyword) ) {
264
					if( @yourls_insert_link_in_db( $url, $keyword, $title ) ){
265
						// everything ok, populate needed vars
266
						$return['url']      = array('keyword' => $keyword, 'url' => $strip_url, 'title' => $title, 'date' => $timestamp, 'ip' => $ip );
267
						$return['status']   = 'success';
268
						$return['message']  = /* //translators: eg "http://someurl/ added to DB" */ yourls_s( '%s added to database', yourls_trim_long_string( $strip_url ) );
269
						$return['title']    = $title;
270
						$return['html']     = yourls_table_add_row( $keyword, $url, $title, $ip, 0, time() );
271
						$return['shorturl'] = YOURLS_SITE .'/'. $keyword;
272
					}else{
273
						// database error, couldnt store result
274
						$return['status']   = 'fail';
275
						$return['code']     = 'error:db';
276
						$return['message']  = yourls_s( 'Error saving url to database' );
277
					}
278
					$ok = true;
279
				}
280
				$id++;
281
			} while ( !$ok );
282
			@yourls_update_next_decimal( $id );
283
		}
284
285
	// URL was already stored
286
	} else {
287
288
		yourls_do_action( 'add_new_link_already_stored', $url, $keyword, $title );
289
290
		$return['status']   = 'fail';
291
		$return['code']     = 'error:url';
292
		$return['url']      = array( 'keyword' => $url_exists->keyword, 'url' => $strip_url, 'title' => $url_exists->title, 'date' => $url_exists->timestamp, 'ip' => $url_exists->ip, 'clicks' => $url_exists->clicks );
293
		$return['message']  = /* //translators: eg "http://someurl/ already exists" */ yourls_s( '%s already exists in database', yourls_trim_long_string( $strip_url ) );
294
		$return['title']    = $url_exists->title;
295
		$return['shorturl'] = YOURLS_SITE .'/'. $url_exists->keyword;
296
	}
297
298
	yourls_do_action( 'post_add_new_link', $url, $keyword, $title );
299
300
	$return['statusCode'] = 200; // regardless of result, this is still a valid request
301
	return yourls_apply_filter( 'add_new_link', $return, $url, $keyword, $title );
302
}
303
304
305
/**
306
 * Edit a link
307
 *
308
 */
309
function yourls_edit_link( $url, $keyword, $newkeyword='', $title='' ) {
310
	// Allow plugins to short-circuit the whole function
311
	$pre = yourls_apply_filter( 'shunt_edit_link', null, $keyword, $url, $keyword, $newkeyword, $title );
312
	if ( null !== $pre )
313
		return $pre;
314
315
	global $ydb;
316
317
	$table = YOURLS_DB_TABLE_URL;
318
	$url = yourls_escape (yourls_sanitize_url( $url ) );
319
	$keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
320
	$title = yourls_escape( yourls_sanitize_title( $title ) );
321
	$newkeyword = yourls_escape( yourls_sanitize_string( $newkeyword ) );
322
	$strip_url = stripslashes( $url );
323
	$strip_title = stripslashes( $title );
324
	$old_url = $ydb->get_var( "SELECT `url` FROM `$table` WHERE `keyword` = '$keyword';" );
325
326
	// Check if new URL is not here already
327
	if ( $old_url != $url && !yourls_allow_duplicate_longurls() ) {
328
		$new_url_already_there = intval($ydb->get_var("SELECT COUNT(keyword) FROM `$table` WHERE `url` = '$url';"));
329
	} else {
330
		$new_url_already_there = false;
331
	}
332
333
	// Check if the new keyword is not here already
334
	if ( $newkeyword != $keyword ) {
335
		$keyword_is_ok = yourls_keyword_is_free( $newkeyword );
336
	} else {
337
		$keyword_is_ok = true;
338
	}
339
340
	yourls_do_action( 'pre_edit_link', $url, $keyword, $newkeyword, $new_url_already_there, $keyword_is_ok );
341
342
	// All clear, update
343
	if ( ( !$new_url_already_there || yourls_allow_duplicate_longurls() ) && $keyword_is_ok ) {
344
			$update_url = $ydb->query( "UPDATE `$table` SET `url` = '$url', `keyword` = '$newkeyword', `title` = '$title' WHERE `keyword` = '$keyword';" );
345
		if( $update_url ) {
346
			$return['url']     = array( 'keyword' => $newkeyword, 'shorturl' => YOURLS_SITE.'/'.$newkeyword, 'url' => $strip_url, 'display_url' => yourls_trim_long_string( $strip_url ), 'title' => $strip_title, 'display_title' => yourls_trim_long_string( $strip_title ) );
347
			$return['status']  = 'success';
348
			$return['message'] = yourls__( 'Link updated in database' );
349
		} else {
350
			$return['status']  = 'fail';
351
			$return['message'] = /* //translators: "Error updating http://someurl/ (Shorturl: http://sho.rt/blah)" */ yourls_s( 'Error updating %s (Short URL: %s)', yourls_trim_long_string( $strip_url ), $keyword ) ;
352
		}
353
354
	// Nope
355
	} else {
356
		$return['status']  = 'fail';
357
		$return['message'] = yourls__( 'URL or keyword already exists in database' );
358
	}
359
360
	return yourls_apply_filter( 'edit_link', $return, $url, $keyword, $newkeyword, $title, $new_url_already_there, $keyword_is_ok );
361
}
362
363
/**
364
 * Update a title link (no checks for duplicates etc..)
365
 *
366
 */
367 View Code Duplication
function yourls_edit_link_title( $keyword, $title ) {
368
	// Allow plugins to short-circuit the whole function
369
	$pre = yourls_apply_filter( 'shunt_edit_link_title', null, $keyword, $title );
370
	if ( null !== $pre )
371
		return $pre;
372
373
	global $ydb;
374
375
	$keyword = yourls_escape( yourls_sanitize_keyword( $keyword ) );
376
	$title = yourls_escape( yourls_sanitize_title( $title ) );
377
378
	$table = YOURLS_DB_TABLE_URL;
379
	$update = $ydb->query("UPDATE `$table` SET `title` = '$title' WHERE `keyword` = '$keyword';");
380
381
	return $update;
382
}
383
384
385
/**
386
 * Check if keyword id is free (ie not already taken, and not reserved). Return bool.
387
 *
388
 */
389
function yourls_keyword_is_free( $keyword ) {
390
	$free = true;
391
	if ( yourls_keyword_is_reserved( $keyword ) or yourls_keyword_is_taken( $keyword ) )
392
		$free = false;
393
394
	return yourls_apply_filter( 'keyword_is_free', $free, $keyword );
395
}
396
397
/**
398
 * Check if a keyword is taken (ie there is already a short URL with this id). Return bool.
399
 *
400
 */
401 View Code Duplication
function yourls_keyword_is_taken( $keyword ) {
402
403
	// Allow plugins to short-circuit the whole function
404
	$pre = yourls_apply_filter( 'shunt_keyword_is_taken', false, $keyword );
405
	if ( false !== $pre )
406
		return $pre;
407
408
	global $ydb;
409
	$keyword = yourls_escape( yourls_sanitize_keyword( $keyword ) );
410
	$taken = false;
411
	$table = YOURLS_DB_TABLE_URL;
412
	$already_exists = $ydb->get_var( "SELECT COUNT(`keyword`) FROM `$table` WHERE `keyword` = '$keyword';" );
413
	if ( $already_exists )
414
		$taken = true;
415
416
	return yourls_apply_filter( 'keyword_is_taken', $taken, $keyword );
417
}
418
419
/**
420
 * Return XML output.
421
 *
422
 */
423
function yourls_xml_encode( $array ) {
424
	require_once( YOURLS_INC.'/functions-xml.php' );
425
	$converter= new yourls_array2xml;
426
	return $converter->array2xml( $array );
427
}
428
429
/**
430
 * Return array of all information associated with keyword. Returns false if keyword not found. Set optional $use_cache to false to force fetching from DB
431
 *
432
 */
433
function yourls_get_keyword_infos( $keyword, $use_cache = true ) {
434
	global $ydb;
435
	$keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
436
437
	yourls_do_action( 'pre_get_keyword', $keyword, $use_cache );
438
439
	if( isset( $ydb->infos[$keyword] ) && $use_cache == true ) {
440
		return yourls_apply_filter( 'get_keyword_infos', $ydb->infos[$keyword], $keyword );
441
	}
442
443
	yourls_do_action( 'get_keyword_not_cached', $keyword );
444
445
	$table = YOURLS_DB_TABLE_URL;
446
	$infos = $ydb->get_row( "SELECT * FROM `$table` WHERE `keyword` = '$keyword'" );
447
448
	if( $infos ) {
449
		$infos = (array)$infos;
450
		$ydb->infos[ $keyword ] = $infos;
451
	} else {
452
		$ydb->infos[ $keyword ] = false;
453
	}
454
455
	return yourls_apply_filter( 'get_keyword_infos', $ydb->infos[$keyword], $keyword );
456
}
457
458
/**
459
 * Return (string) selected information associated with a keyword. Optional $notfound = string default message if nothing found
460
 *
461
 */
462
function yourls_get_keyword_info( $keyword, $field, $notfound = false ) {
463
464
	// Allow plugins to short-circuit the whole function
465
	$pre = yourls_apply_filter( 'shunt_get_keyword_info', false, $keyword, $field, $notfound );
466
	if ( false !== $pre )
467
		return $pre;
468
469
	$keyword = yourls_sanitize_string( $keyword );
470
	$infos = yourls_get_keyword_infos( $keyword );
471
472
	$return = $notfound;
473
	if ( isset( $infos[ $field ] ) && $infos[ $field ] !== false )
474
		$return = $infos[ $field ];
475
476
	return yourls_apply_filter( 'get_keyword_info', $return, $keyword, $field, $notfound );
477
}
478
479
/**
480
 * Return title associated with keyword. Optional $notfound = string default message if nothing found
481
 *
482
 */
483
function yourls_get_keyword_title( $keyword, $notfound = false ) {
484
	return yourls_get_keyword_info( $keyword, 'title', $notfound );
485
}
486
487
/**
488
 * Return long URL associated with keyword. Optional $notfound = string default message if nothing found
489
 *
490
 */
491
function yourls_get_keyword_longurl( $keyword, $notfound = false ) {
492
	return yourls_get_keyword_info( $keyword, 'url', $notfound );
493
}
494
495
/**
496
 * Return number of clicks on a keyword. Optional $notfound = string default message if nothing found
497
 *
498
 */
499
function yourls_get_keyword_clicks( $keyword, $notfound = false ) {
500
	return yourls_get_keyword_info( $keyword, 'clicks', $notfound );
501
}
502
503
/**
504
 * Return IP that added a keyword. Optional $notfound = string default message if nothing found
505
 *
506
 */
507
function yourls_get_keyword_IP( $keyword, $notfound = false ) {
508
	return yourls_get_keyword_info( $keyword, 'ip', $notfound );
509
}
510
511
/**
512
 * Return timestamp associated with a keyword. Optional $notfound = string default message if nothing found
513
 *
514
 */
515
function yourls_get_keyword_timestamp( $keyword, $notfound = false ) {
516
	return yourls_get_keyword_info( $keyword, 'timestamp', $notfound );
517
}
518
519
/**
520
 * Update click count on a short URL. Return 0/1 for error/success.
521
 *
522
 */
523
function yourls_update_clicks( $keyword, $clicks = false ) {
524
	// Allow plugins to short-circuit the whole function
525
	$pre = yourls_apply_filter( 'shunt_update_clicks', false, $keyword, $clicks );
526
	if ( false !== $pre )
527
		return $pre;
528
529
	global $ydb;
530
	$keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
531
	$table = YOURLS_DB_TABLE_URL;
532
	if ( $clicks !== false && is_int( $clicks ) && $clicks >= 0 )
533
		$update = $ydb->query( "UPDATE `$table` SET `clicks` = $clicks WHERE `keyword` = '$keyword'" );
534
	else
535
		$update = $ydb->query( "UPDATE `$table` SET `clicks` = clicks + 1 WHERE `keyword` = '$keyword'" );
536
537
	yourls_do_action( 'update_clicks', $keyword, $update, $clicks );
538
	return $update;
539
}
540
541
/**
542
 * Return array of stats. (string)$filter is 'bottom', 'last', 'rand' or 'top'. (int)$limit is the number of links to return
543
 *
544
 */
545
function yourls_get_stats( $filter = 'top', $limit = 10, $start = 0 ) {
546
	global $ydb;
547
548
	switch( $filter ) {
549
		case 'bottom':
550
			$sort_by    = 'clicks';
551
			$sort_order = 'asc';
552
			break;
553
		case 'last':
554
			$sort_by    = 'timestamp';
555
			$sort_order = 'desc';
556
			break;
557
		case 'rand':
558
		case 'random':
559
			$sort_by    = 'RAND()';
560
			$sort_order = '';
561
			break;
562
		case 'top':
563
		default:
564
			$sort_by    = 'clicks';
565
			$sort_order = 'desc';
566
			break;
567
	}
568
569
	// Fetch links
570
	$limit = intval( $limit );
571
	$start = intval( $start );
572
	if ( $limit > 0 ) {
573
574
		$table_url = YOURLS_DB_TABLE_URL;
575
		$results = $ydb->get_results( "SELECT * FROM `$table_url` WHERE 1=1 ORDER BY `$sort_by` $sort_order LIMIT $start, $limit;" );
576
577
		$return = array();
578
		$i = 1;
579
580
		foreach ( (array)$results as $res ) {
581
			$return['links']['link_'.$i++] = array(
582
				'shorturl' => YOURLS_SITE .'/'. $res->keyword,
583
				'url'      => $res->url,
584
				'title'    => $res->title,
585
				'timestamp'=> $res->timestamp,
586
				'ip'       => $res->ip,
587
				'clicks'   => $res->clicks,
588
			);
589
		}
590
	}
591
592
	$return['stats'] = yourls_get_db_stats();
593
594
	$return['statusCode'] = 200;
595
596
	return yourls_apply_filter( 'get_stats', $return, $filter, $limit, $start );
597
}
598
599
/**
600
 * Return array of stats. (string)$filter is 'bottom', 'last', 'rand' or 'top'. (int)$limit is the number of links to return
601
 *
602
 */
603
function yourls_get_link_stats( $shorturl ) {
604
	global $ydb;
605
606
	$table_url = YOURLS_DB_TABLE_URL;
607
	$shorturl  = yourls_escape( yourls_sanitize_keyword( $shorturl ) );
608
609
	$res = $ydb->get_row( "SELECT * FROM `$table_url` WHERE keyword = '$shorturl';" );
610
	$return = array();
611
612
	if( !$res ) {
613
		// non existent link
614
		$return = array(
615
			'statusCode' => 404,
616
			'message'    => 'Error: short URL not found',
617
		);
618
	} else {
619
		$return = array(
620
			'statusCode' => 200,
621
			'message'    => 'success',
622
			'link'       => array(
623
				'shorturl' => YOURLS_SITE .'/'. $res->keyword,
624
				'url'      => $res->url,
625
				'title'    => $res->title,
626
				'timestamp'=> $res->timestamp,
627
				'ip'       => $res->ip,
628
				'clicks'   => $res->clicks,
629
			)
630
		);
631
	}
632
633
	return yourls_apply_filter( 'get_link_stats', $return, $shorturl );
634
}
635
636
/**
637
 * Get total number of URLs and sum of clicks. Input: optional "AND WHERE" clause. Returns array
638
 *
639
 * IMPORTANT NOTE: make sure arguments for the $where clause have been sanitized and yourls_escape()'d
640
 * before calling this function.
641
 *
642
 */
643
function yourls_get_db_stats( $where = '' ) {
644
	global $ydb;
645
	$table_url = YOURLS_DB_TABLE_URL;
646
647
	$totals = $ydb->get_row( "SELECT COUNT(keyword) as count, SUM(clicks) as sum FROM `$table_url` WHERE 1=1 $where" );
648
	$return = array( 'total_links' => $totals->count, 'total_clicks' => $totals->sum );
649
650
	return yourls_apply_filter( 'get_db_stats', $return, $where );
651
}
652
653
/**
654
 * Get number of SQL queries performed
655
 *
656
 */
657
function yourls_get_num_queries() {
658
	global $ydb;
659
660
	return yourls_apply_filter( 'get_num_queries', $ydb->num_queries );
661
}
662
663
/**
664
 * Returns a sanitized a user agent string. Given what I found on http://www.user-agents.org/ it should be OK.
665
 *
666
 */
667
function yourls_get_user_agent() {
668
	if ( !isset( $_SERVER['HTTP_USER_AGENT'] ) )
669
		return '-';
670
671
	$ua = strip_tags( html_entity_decode( $_SERVER['HTTP_USER_AGENT'] ));
672
	$ua = preg_replace('![^0-9a-zA-Z\':., /{}\(\)\[\]\+@&\!\?;_\-=~\*\#]!', '', $ua );
673
674
	return yourls_apply_filter( 'get_user_agent', substr( $ua, 0, 254 ) );
675
}
676
677
/**
678
 * Redirect to another page
679
 *
680
 */
681
function yourls_redirect( $location, $code = 301 ) {
682
	yourls_do_action( 'pre_redirect', $location, $code );
683
	$location = yourls_apply_filter( 'redirect_location', $location, $code );
684
	$code     = yourls_apply_filter( 'redirect_code', $code, $location );
685
	// Redirect, either properly if possible, or via Javascript otherwise
686
	if( !headers_sent() ) {
687
		yourls_status_header( $code );
688
		header( "Location: $location" );
689
	} else {
690
		yourls_redirect_javascript( $location );
691
	}
692
	die();
693
}
694
695
/**
696
 * Set HTTP status header
697
 *
698
 * @since 1.4
699
 * @param int $code  status header code
700
 * @return bool      whether header was sent
701
 */
702
function yourls_status_header( $code = 200 ) {
703
	yourls_do_action( 'status_header', $code );
704
705
	if( headers_sent() )
706
		return false;
707
708
	$protocol = $_SERVER['SERVER_PROTOCOL'];
709
	if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol )
710
		$protocol = 'HTTP/1.0';
711
712
	$code = intval( $code );
713
	$desc = yourls_get_HTTP_status( $code );
714
715
	@header ("$protocol $code $desc"); // This causes problems on IIS and some FastCGI setups
716
717
    return true;
718
}
719
720
/**
721
 * Redirect to another page using Javascript. Set optional (bool)$dontwait to false to force manual redirection (make sure a message has been read by user)
722
 *
723
 */
724
function yourls_redirect_javascript( $location, $dontwait = true ) {
725
	yourls_do_action( 'pre_redirect_javascript', $location, $dontwait );
726
	$location = yourls_apply_filter( 'redirect_javascript', $location, $dontwait );
727
	if( $dontwait ) {
728
		$message = yourls_s( 'if you are not redirected after 10 seconds, please <a href="%s">click here</a>', $location );
729
		echo <<<REDIR
730
		<script type="text/javascript">
731
		window.location="$location";
732
		</script>
733
		<small>($message)</small>
734
REDIR;
735
	} else {
736
		echo '<p>' . yourls_s( 'Please <a href="%s">click here</a>', $location ) . '</p>';
737
	}
738
	yourls_do_action( 'post_redirect_javascript', $location );
739
}
740
741
/**
742
 * Return a HTTP status code
743
 *
744
 */
745
function yourls_get_HTTP_status( $code ) {
746
	$code = intval( $code );
747
	$headers_desc = array(
748
		100 => 'Continue',
749
		101 => 'Switching Protocols',
750
		102 => 'Processing',
751
752
		200 => 'OK',
753
		201 => 'Created',
754
		202 => 'Accepted',
755
		203 => 'Non-Authoritative Information',
756
		204 => 'No Content',
757
		205 => 'Reset Content',
758
		206 => 'Partial Content',
759
		207 => 'Multi-Status',
760
		226 => 'IM Used',
761
762
		300 => 'Multiple Choices',
763
		301 => 'Moved Permanently',
764
		302 => 'Found',
765
		303 => 'See Other',
766
		304 => 'Not Modified',
767
		305 => 'Use Proxy',
768
		306 => 'Reserved',
769
		307 => 'Temporary Redirect',
770
771
		400 => 'Bad Request',
772
		401 => 'Unauthorized',
773
		402 => 'Payment Required',
774
		403 => 'Forbidden',
775
		404 => 'Not Found',
776
		405 => 'Method Not Allowed',
777
		406 => 'Not Acceptable',
778
		407 => 'Proxy Authentication Required',
779
		408 => 'Request Timeout',
780
		409 => 'Conflict',
781
		410 => 'Gone',
782
		411 => 'Length Required',
783
		412 => 'Precondition Failed',
784
		413 => 'Request Entity Too Large',
785
		414 => 'Request-URI Too Long',
786
		415 => 'Unsupported Media Type',
787
		416 => 'Requested Range Not Satisfiable',
788
		417 => 'Expectation Failed',
789
		422 => 'Unprocessable Entity',
790
		423 => 'Locked',
791
		424 => 'Failed Dependency',
792
		426 => 'Upgrade Required',
793
794
		500 => 'Internal Server Error',
795
		501 => 'Not Implemented',
796
		502 => 'Bad Gateway',
797
		503 => 'Service Unavailable',
798
		504 => 'Gateway Timeout',
799
		505 => 'HTTP Version Not Supported',
800
		506 => 'Variant Also Negotiates',
801
		507 => 'Insufficient Storage',
802
		510 => 'Not Extended'
803
	);
804
805
	if ( isset( $headers_desc[$code] ) )
806
		return $headers_desc[$code];
807
	else
808
		return '';
809
}
810
811
/**
812
 * Log a redirect (for stats)
813
 *
814
 * This function does not check for the existence of a valid keyword, in order to save a query. Make sure the keyword
815
 * exists before calling it.
816
 *
817
 * @since 1.4
818
 * @param string $keyword short URL keyword
819
 * @return mixed Result of the INSERT query (1 on success)
820
 */
821
function yourls_log_redirect( $keyword ) {
822
	// Allow plugins to short-circuit the whole function
823
	$pre = yourls_apply_filter( 'shunt_log_redirect', false, $keyword );
824
	if ( false !== $pre )
825
		return $pre;
826
827
	if ( !yourls_do_log_redirect() )
828
		return true;
829
830
	global $ydb;
831
	$table = YOURLS_DB_TABLE_LOG;
832
833
    $now      = date( 'Y-m-d H:i:s' );
834
	$keyword  = yourls_escape( yourls_sanitize_string( $keyword ) );
835
	$referrer = ( isset( $_SERVER['HTTP_REFERER'] ) ? yourls_escape( yourls_sanitize_url_safe( $_SERVER['HTTP_REFERER'] ) ) : 'direct' );
836
	$ua       = yourls_escape( yourls_get_user_agent() );
837
	$ip       = yourls_escape( yourls_get_IP() );
838
	$location = yourls_escape( yourls_geo_ip_to_countrycode( $ip ) );
839
840
	return $ydb->query( "INSERT INTO `$table` (click_time, shorturl, referrer, user_agent, ip_address, country_code) VALUES ('$now', '$keyword', '$referrer', '$ua', '$ip', '$location')" );
841
}
842
843
/**
844
 * Check if we want to not log redirects (for stats)
845
 *
846
 */
847
function yourls_do_log_redirect() {
848
	return ( !defined( 'YOURLS_NOSTATS' ) || YOURLS_NOSTATS != true );
849
}
850
851
/**
852
 * Converts an IP to a 2 letter country code, using GeoIP database if available in includes/geo/
853
 *
854
 * @since 1.4
855
 * @param string $ip IP or, if empty string, will be current user IP
856
 * @param string $defaut Default string to return if IP doesn't resolve to a country (malformed, private IP...)
857
 * @return string 2 letter country code (eg 'US') or $default
858
 */
859
function yourls_geo_ip_to_countrycode( $ip = '', $default = '' ) {
860
	// Allow plugins to short-circuit the Geo IP API
861
	$location = yourls_apply_filter( 'shunt_geo_ip_to_countrycode', false, $ip, $default ); // at this point $ip can be '', check if your plugin hooks in here
862
	if ( false !== $location )
863
		return $location;
864
865
	if ( $ip == '' )
866
		$ip = yourls_get_IP();
867
868
	// Use IPv4 or IPv6 DB & functions
869
	if( false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
870
		$db   = 'GeoIP.dat';
871
		$func = 'geoip_country_code_by_addr';
872
	} else {
873
		$db   = 'GeoIPv6.dat';
874
		$func = 'geoip_country_code_by_addr_v6';
875
	}
876
877
	if ( !file_exists( YOURLS_INC . '/geo/' . $db ) || !file_exists( YOURLS_INC .'/geo/geoip.inc' ) )
878
		return $default;
879
880
	require_once( YOURLS_INC . '/geo/geoip.inc' );
881
	$gi = geoip_open( YOURLS_INC . '/geo/' . $db, GEOIP_STANDARD );
882
	try {
883
		$location = call_user_func( $func, $gi, $ip );
884
	} catch ( Exception $e ) {
885
		$location = '';
886
	}
887
	geoip_close( $gi );
888
889
	if( '' == $location )
890
		$location = $default;
891
892
	return yourls_apply_filter( 'geo_ip_to_countrycode', $location, $ip, $default );
893
}
894
895
/**
896
 * Converts a 2 letter country code to long name (ie AU -> Australia)
897
 *
898
 */
899
function yourls_geo_countrycode_to_countryname( $code ) {
900
	// Allow plugins to short-circuit the Geo IP API
901
	$country = yourls_apply_filter( 'shunt_geo_countrycode_to_countryname', false, $code );
902
	if ( false !== $country )
903
		return $country;
904
905
	// Load the Geo class if not already done
906
	if( !class_exists( 'GeoIP', false ) ) {
907
		$temp = yourls_geo_ip_to_countrycode( '127.0.0.1' );
0 ignored issues
show
$temp is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
908
	}
909
910
	if( class_exists( 'GeoIP', false ) ) {
911
		$geo  = new GeoIP;
912
		$id   = $geo->GEOIP_COUNTRY_CODE_TO_NUMBER[ $code ];
913
		$long = $geo->GEOIP_COUNTRY_NAMES[ $id ];
914
		return $long;
915
	} else {
916
		return false;
917
	}
918
}
919
920
/**
921
 * Return flag URL from 2 letter country code
922
 *
923
 */
924
function yourls_geo_get_flag( $code ) {
925
	if( file_exists( YOURLS_INC.'/geo/flags/flag_'.strtolower($code).'.gif' ) ) {
926
		$img = yourls_match_current_protocol( YOURLS_SITE.'/includes/geo/flags/flag_'.( strtolower( $code ) ).'.gif' );
927
	} else {
928
		$img = false;
929
	}
930
	return yourls_apply_filter( 'geo_get_flag', $img, $code );
931
}
932
933
934
/**
935
 * Check if an upgrade is needed
936
 *
937
 */
938
function yourls_upgrade_is_needed() {
939
	// check YOURLS_DB_VERSION exist && match values stored in YOURLS_DB_TABLE_OPTIONS
940
	list( $currentver, $currentsql ) = yourls_get_current_version_from_sql();
941
	if( $currentsql < YOURLS_DB_VERSION )
942
		return true;
943
944
	return false;
945
}
946
947
/**
948
 * Get current version & db version as stored in the options DB. Prior to 1.4 there's no option table.
949
 *
950
 */
951
function yourls_get_current_version_from_sql() {
952
	$currentver = yourls_get_option( 'version' );
953
	$currentsql = yourls_get_option( 'db_version' );
954
955
	// Values if version is 1.3
956
	if( !$currentver )
957
		$currentver = '1.3';
958
	if( !$currentsql )
959
		$currentsql = '100';
960
961
	return array( $currentver, $currentsql);
962
}
963
964
/**
965
 * Read an option from DB (or from cache if available). Return value or $default if not found
966
 *
967
 * Pretty much stolen from WordPress
968
 *
969
 * @since 1.4
970
 * @param string $option Option name. Expected to not be SQL-escaped.
971
 * @param mixed $default Optional value to return if option doesn't exist. Default false.
972
 * @return mixed Value set for the option.
973
 */
974
function yourls_get_option( $option_name, $default = false ) {
975
	global $ydb;
976
977
	// Allow plugins to short-circuit options
978
	$pre = yourls_apply_filter( 'shunt_option_'.$option_name, false );
979
	if ( false !== $pre )
980
		return $pre;
981
982
	// If option not cached already, get its value from the DB
983
	if ( !isset( $ydb->option[$option_name] ) ) {
984
		$table = YOURLS_DB_TABLE_OPTIONS;
985
		$option_name = yourls_escape( $option_name );
986
		$row = $ydb->get_row( "SELECT `option_value` FROM `$table` WHERE `option_name` = '$option_name' LIMIT 1" );
987
		if ( is_object( $row) ) { // Has to be get_row instead of get_var because of funkiness with 0, false, null values
988
			$value = $row->option_value;
989
		} else { // option does not exist, so we must cache its non-existence
990
			$value = $default;
991
		}
992
		$ydb->option[ $option_name ] = yourls_maybe_unserialize( $value );
993
	}
994
995
	return yourls_apply_filter( 'get_option_'.$option_name, $ydb->option[$option_name] );
996
}
997
998
/**
999
 * Read all options from DB at once
1000
 *
1001
 * The goal is to read all options at once and then populate array $ydb->option, to prevent further
1002
 * SQL queries if we need to read an option value later.
1003
 * It's also a simple check whether YOURLS is installed or not (no option = assuming not installed) after
1004
 * a check for DB server reachability has been performed
1005
 *
1006
 * @since 1.4
1007
 */
1008
function yourls_get_all_options() {
1009
	global $ydb;
1010
1011
	// Allow plugins to short-circuit all options. (Note: regular plugins are loaded after all options)
1012
	$pre = yourls_apply_filter( 'shunt_all_options', false );
1013
	if ( false !== $pre )
1014
		return $pre;
1015
1016
	$table = YOURLS_DB_TABLE_OPTIONS;
1017
1018
	$allopt = $ydb->get_results( "SELECT `option_name`, `option_value` FROM `$table` WHERE 1=1" );
1019
1020
	foreach( (array)$allopt as $option ) {
1021
		$ydb->option[ $option->option_name ] = yourls_maybe_unserialize( $option->option_value );
1022
	}
1023
1024
	if( property_exists( $ydb, 'option' ) ) {
1025
		$ydb->option = yourls_apply_filter( 'get_all_options', $ydb->option );
1026
		$ydb->installed = true;
1027
	} else {
1028
		// Zero option found: either YOURLS is not installed or DB server is dead
1029
        if( !yourls_is_db_alive() ) {
1030
            yourls_db_dead(); // YOURLS will die here
1031
        }
1032
        $ydb->installed = false;
1033
	}
1034
}
1035
1036
/**
1037
 * Update (add if doesn't exist) an option to DB
1038
 *
1039
 * Pretty much stolen from WordPress
1040
 *
1041
 * @since 1.4
1042
 * @param string $option Option name. Expected to not be SQL-escaped.
1043
 * @param mixed $newvalue Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
1044
 * @return bool False if value was not updated, true otherwise.
1045
 */
1046
function yourls_update_option( $option_name, $newvalue ) {
1047
	global $ydb;
1048
	$table = YOURLS_DB_TABLE_OPTIONS;
1049
1050
	$option_name = trim( $option_name );
1051
	if ( empty( $option_name ) )
1052
		return false;
1053
1054
	// Use clone to break object refs -- see commit 09b989d375bac65e692277f61a84fede2fb04ae3
1055
	if ( is_object( $newvalue ) )
1056
		$newvalue = clone $newvalue;
1057
1058
	$option_name = yourls_escape( $option_name );
1059
1060
	$oldvalue = yourls_get_option( $option_name );
1061
1062
	// If the new and old values are the same, no need to update.
1063
	if ( $newvalue === $oldvalue )
1064
		return false;
1065
1066
	if ( false === $oldvalue ) {
1067
		yourls_add_option( $option_name, $newvalue );
1068
		return true;
1069
	}
1070
1071
	$_newvalue = yourls_escape( yourls_maybe_serialize( $newvalue ) );
1072
1073
	yourls_do_action( 'update_option', $option_name, $oldvalue, $newvalue );
1074
1075
	$ydb->query( "UPDATE `$table` SET `option_value` = '$_newvalue' WHERE `option_name` = '$option_name'" );
1076
1077
	if ( $ydb->rows_affected == 1 ) {
1078
		$ydb->option[ $option_name ] = $newvalue;
1079
		return true;
1080
	}
1081
	return false;
1082
}
1083
1084
/**
1085
 * Add an option to the DB
1086
 *
1087
 * Pretty much stolen from WordPress
1088
 *
1089
 * @since 1.4
1090
 * @param string $option Name of option to add. Expected to not be SQL-escaped.
1091
 * @param mixed $value Optional option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
1092
 * @return bool False if option was not added and true otherwise.
1093
 */
1094
function yourls_add_option( $name, $value = '' ) {
1095
	global $ydb;
1096
	$table = YOURLS_DB_TABLE_OPTIONS;
1097
1098
	$name = trim( $name );
1099
	if ( empty( $name ) )
1100
		return false;
1101
1102
	// Use clone to break object refs -- see commit 09b989d375bac65e692277f61a84fede2fb04ae3
1103
	if ( is_object( $value ) )
1104
		$value = clone $value;
1105
1106
	$name = yourls_escape( $name );
1107
1108
	// Make sure the option doesn't already exist
1109
	if ( false !== yourls_get_option( $name ) )
1110
		return false;
1111
1112
	$_value = yourls_escape( yourls_maybe_serialize( $value ) );
1113
1114
	yourls_do_action( 'add_option', $name, $_value );
1115
1116
	$ydb->query( "INSERT INTO `$table` (`option_name`, `option_value`) VALUES ('$name', '$_value')" );
1117
	$ydb->option[ $name ] = $value;
1118
	return true;
1119
}
1120
1121
1122
/**
1123
 * Delete an option from the DB
1124
 *
1125
 * Pretty much stolen from WordPress
1126
 *
1127
 * @since 1.4
1128
 * @param string $option Option name to delete. Expected to not be SQL-escaped.
1129
 * @return bool True, if option is successfully deleted. False on failure.
1130
 */
1131
function yourls_delete_option( $name ) {
1132
	global $ydb;
1133
	$table = YOURLS_DB_TABLE_OPTIONS;
1134
	$name = yourls_escape( $name );
1135
1136
	// Get the ID, if no ID then return
1137
	$option = $ydb->get_row( "SELECT option_id FROM `$table` WHERE `option_name` = '$name'" );
1138
	if ( is_null( $option ) || !$option->option_id )
1139
		return false;
1140
1141
	yourls_do_action( 'delete_option', $name );
1142
1143
	$ydb->query( "DELETE FROM `$table` WHERE `option_name` = '$name'" );
1144
	unset( $ydb->option[ $name ] );
1145
	return true;
1146
}
1147
1148
1149
/**
1150
 * Serialize data if needed. Stolen from WordPress
1151
 *
1152
 * @since 1.4
1153
 * @param mixed $data Data that might be serialized.
1154
 * @return mixed A scalar data
1155
 */
1156
function yourls_maybe_serialize( $data ) {
1157
	if ( is_array( $data ) || is_object( $data ) )
1158
		return serialize( $data );
1159
1160
	if ( yourls_is_serialized( $data, false ) )
1161
		return serialize( $data );
1162
1163
	return $data;
1164
}
1165
1166
/**
1167
 * Check value to find if it was serialized. Stolen from WordPress
1168
 *
1169
 * @since 1.4
1170
 * @param mixed $data Value to check to see if was serialized.
1171
 * @param bool $strict Optional. Whether to be strict about the end of the string. Defaults true.
1172
 * @return bool False if not serialized and true if it was.
1173
 */
1174
function yourls_is_serialized( $data, $strict = true ) {
1175
	// if it isn't a string, it isn't serialized
1176
	if ( ! is_string( $data ) )
1177
		return false;
1178
	$data = trim( $data );
1179
	 if ( 'N;' == $data )
1180
		return true;
1181
	$length = strlen( $data );
1182
	if ( $length < 4 )
1183
		return false;
1184
	if ( ':' !== $data[1] )
1185
		return false;
1186
	if ( $strict ) {
1187
		$lastc = $data[ $length - 1 ];
1188
		if ( ';' !== $lastc && '}' !== $lastc )
1189
			return false;
1190
	} else {
1191
		$semicolon = strpos( $data, ';' );
1192
		$brace	 = strpos( $data, '}' );
1193
		// Either ; or } must exist.
1194
		if ( false === $semicolon && false === $brace )
1195
			return false;
1196
		// But neither must be in the first X characters.
1197
		if ( false !== $semicolon && $semicolon < 3 )
1198
			return false;
1199
		if ( false !== $brace && $brace < 4 )
1200
			return false;
1201
	}
1202
	$token = $data[0];
1203
	switch ( $token ) {
1204
		case 's' :
1205
			if ( $strict ) {
1206
				if ( '"' !== $data[ $length - 2 ] )
1207
					return false;
1208
			} elseif ( false === strpos( $data, '"' ) ) {
1209
				return false;
1210
			}
1211
			// or else fall through
1212
		case 'a' :
1213
		case 'O' :
1214
			return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
1215
		case 'b' :
1216
		case 'i' :
1217
		case 'd' :
1218
			$end = $strict ? '$' : '';
1219
			return (bool) preg_match( "/^{$token}:[0-9.E-]+;$end/", $data );
1220
	}
1221
	return false;
1222
}
1223
1224
/**
1225
 * Unserialize value only if it was serialized. Stolen from WP
1226
 *
1227
 * @since 1.4
1228
 * @param string $original Maybe unserialized original, if is needed.
1229
 * @return mixed Unserialized data can be any type.
1230
 */
1231
function yourls_maybe_unserialize( $original ) {
1232
	if ( yourls_is_serialized( $original ) ) // don't attempt to unserialize data that wasn't serialized going in
1233
		return @unserialize( $original );
1234
	return $original;
1235
}
1236
1237
/**
1238
 * Determine if the current page is private
1239
 *
1240
 */
1241
function yourls_is_private() {
1242
	$private = false;
1243
1244
	if ( defined('YOURLS_PRIVATE') && YOURLS_PRIVATE == true ) {
1245
1246
		// Allow overruling for particular pages:
1247
1248
		// API
1249
		if( yourls_is_API() ) {
1250
			if( !defined('YOURLS_PRIVATE_API') || YOURLS_PRIVATE_API != false )
1251
				$private = true;
1252
1253
		// Infos
1254
		} elseif( yourls_is_infos() ) {
1255
			if( !defined('YOURLS_PRIVATE_INFOS') || YOURLS_PRIVATE_INFOS !== false )
1256
				$private = true;
1257
1258
		// Others
1259
		} else {
1260
			$private = true;
1261
		}
1262
1263
	}
1264
1265
	return yourls_apply_filter( 'is_private', $private );
1266
}
1267
1268
/**
1269
 * Show login form if required
1270
 *
1271
 */
1272
function yourls_maybe_require_auth() {
1273
	if( yourls_is_private() ) {
1274
		yourls_do_action( 'require_auth' );
1275
		require_once( YOURLS_INC.'/auth.php' );
1276
	} else {
1277
		yourls_do_action( 'require_no_auth' );
1278
	}
1279
}
1280
1281
/**
1282
 * Allow several short URLs for the same long URL ?
1283
 *
1284
 */
1285
function yourls_allow_duplicate_longurls() {
1286
	// special treatment if API to check for WordPress plugin requests
1287
	if( yourls_is_API() ) {
1288
		if ( isset($_REQUEST['source']) && $_REQUEST['source'] == 'plugin' )
1289
			return false;
1290
	}
1291
	return ( defined( 'YOURLS_UNIQUE_URLS' ) && YOURLS_UNIQUE_URLS == false );
1292
}
1293
1294
/**
1295
 * Return array of keywords that redirect to the submitted long URL
1296
 *
1297
 * @since 1.7
1298
 * @param string $longurl long url
1299
 * @param string $sort Optional ORDER BY order (can be 'keyword', 'title', 'timestamp' or'clicks')
1300
 * @param string $order Optional SORT order (can be 'ASC' or 'DESC')
1301
 * @return array array of keywords
1302
 */
1303
function yourls_get_longurl_keywords( $longurl, $sort = 'none', $order = 'ASC' ) {
1304
	global $ydb;
1305
	$longurl = yourls_escape( yourls_sanitize_url( $longurl ) );
1306
	$table   = YOURLS_DB_TABLE_URL;
1307
	$query   = "SELECT `keyword` FROM `$table` WHERE `url` = '$longurl'";
1308
1309
	// Ensure sort is a column in database (@TODO: update verification array if database changes)
1310
	if ( in_array( $sort, array('keyword','title','timestamp','clicks') ) ) {
1311
		$query .= " ORDER BY '".$sort."'";
1312
		if ( in_array( $order, array( 'ASC','DESC' ) ) ) {
1313
			$query .= " ".$order;
1314
		}
1315
	}
1316
	return yourls_apply_filter( 'get_longurl_keywords', $ydb->get_col( $query ), $longurl );
1317
}
1318
1319
/**
1320
 * Check if an IP shortens URL too fast to prevent DB flood. Return true, or die.
1321
 *
1322
 */
1323
function yourls_check_IP_flood( $ip = '' ) {
1324
1325
	// Allow plugins to short-circuit the whole function
1326
	$pre = yourls_apply_filter( 'shunt_check_IP_flood', false, $ip );
1327
	if ( false !== $pre )
1328
		return $pre;
1329
1330
	yourls_do_action( 'pre_check_ip_flood', $ip ); // at this point $ip can be '', check it if your plugin hooks in here
1331
1332
	// Raise white flag if installing or if no flood delay defined
1333
	if(
1334
		( defined('YOURLS_FLOOD_DELAY_SECONDS') && YOURLS_FLOOD_DELAY_SECONDS === 0 ) ||
1335
		!defined('YOURLS_FLOOD_DELAY_SECONDS') ||
1336
		yourls_is_installing()
1337
	)
1338
		return true;
1339
1340
	// Don't throttle logged in users
1341
	if( yourls_is_private() ) {
1342
		 if( yourls_is_valid_user() === true )
1343
			return true;
1344
	}
1345
1346
	// Don't throttle whitelist IPs
1347
	if( defined( 'YOURLS_FLOOD_IP_WHITELIST' ) && YOURLS_FLOOD_IP_WHITELIST ) {
1348
		$whitelist_ips = explode( ',', YOURLS_FLOOD_IP_WHITELIST );
1349
		foreach( (array)$whitelist_ips as $whitelist_ip ) {
1350
			$whitelist_ip = trim( $whitelist_ip );
1351
			if ( $whitelist_ip == $ip )
1352
				return true;
1353
		}
1354
	}
1355
1356
	$ip = ( $ip ? yourls_sanitize_ip( $ip ) : yourls_get_IP() );
1357
	$ip = yourls_escape( $ip );
1358
1359
	yourls_do_action( 'check_ip_flood', $ip );
1360
1361
	global $ydb;
1362
	$table = YOURLS_DB_TABLE_URL;
1363
1364
	$lasttime = $ydb->get_var( "SELECT `timestamp` FROM $table WHERE `ip` = '$ip' ORDER BY `timestamp` DESC LIMIT 1" );
1365
	if( $lasttime ) {
1366
		$now = date( 'U' );
1367
		$then = date( 'U', strtotime( $lasttime ) );
1368
		if( ( $now - $then ) <= YOURLS_FLOOD_DELAY_SECONDS ) {
1369
			// Flood!
1370
			yourls_do_action( 'ip_flood', $ip, $now - $then );
1371
			yourls_die( yourls__( 'Too many URLs added too fast. Slow down please.' ), yourls__( 'Forbidden' ), 403 );
1372
		}
1373
	}
1374
1375
	return true;
1376
}
1377
1378
/**
1379
 * Check if YOURLS is installing
1380
 *
1381
 * @return bool
1382
 * @since 1.6
1383
 */
1384
function yourls_is_installing() {
1385
	$installing = defined( 'YOURLS_INSTALLING' ) && YOURLS_INSTALLING == true;
1386
	return yourls_apply_filter( 'is_installing', $installing );
1387
}
1388
1389
/**
1390
 * Check if YOURLS is upgrading
1391
 *
1392
 * @return bool
1393
 * @since 1.6
1394
 */
1395
function yourls_is_upgrading() {
1396
	$upgrading = defined( 'YOURLS_UPGRADING' ) && YOURLS_UPGRADING == true;
1397
	return yourls_apply_filter( 'is_upgrading', $upgrading );
1398
}
1399
1400
1401
/**
1402
 * Check if YOURLS is installed
1403
 *
1404
 * Checks property $ydb->installed that is created by yourls_get_all_options()
1405
 *
1406
 * See inline comment for updating from 1.3 or prior.
1407
 *
1408
 */
1409
function yourls_is_installed() {
1410
	global $ydb;
1411
	$is_installed = ( property_exists( $ydb, 'installed' ) && $ydb->installed == true );
1412
	return yourls_apply_filter( 'is_installed', $is_installed );
1413
1414
	/* Note: this test won't work on YOURLS 1.3 or older (Aug 2009...)
1415
	   Should someone complain that they cannot upgrade directly from
1416
	   1.3 to 1.7: first, laugh at them, then ask them to install 1.6 first.
1417
	*/
1418
}
1419
1420
/**
1421
 * Generate random string of (int)$length length and type $type (see function for details)
1422
 *
1423
 */
1424
function yourls_rnd_string ( $length = 5, $type = 0, $charlist = '' ) {
1425
	$str = '';
1426
	$length = intval( $length );
1427
1428
	// define possible characters
1429
	switch ( $type ) {
1430
1431
		// custom char list, or comply to charset as defined in config
1432
		case '0':
1433
			$possible = $charlist ? $charlist : yourls_get_shorturl_charset() ;
1434
			break;
1435
1436
		// no vowels to make no offending word, no 0/1/o/l to avoid confusion between letters & digits. Perfect for passwords.
1437
		case '1':
1438
			$possible = "23456789bcdfghjkmnpqrstvwxyz";
1439
			break;
1440
1441
		// Same, with lower + upper
1442
		case '2':
1443
			$possible = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ";
1444
			break;
1445
1446
		// all letters, lowercase
1447
		case '3':
1448
			$possible = "abcdefghijklmnopqrstuvwxyz";
1449
			break;
1450
1451
		// all letters, lowercase + uppercase
1452
		case '4':
1453
			$possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1454
			break;
1455
1456
		// all digits & letters lowercase
1457
		case '5':
1458
			$possible = "0123456789abcdefghijklmnopqrstuvwxyz";
1459
			break;
1460
1461
		// all digits & letters lowercase + uppercase
1462
		case '6':
1463
			$possible = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1464
			break;
1465
1466
	}
1467
1468
    $str = substr( str_shuffle( $possible ), 0, $length );
1469
1470
	return yourls_apply_filter( 'rnd_string', $str, $length, $type, $charlist );
1471
}
1472
1473
/**
1474
 * Return salted string
1475
 *
1476
 */
1477
function yourls_salt( $string ) {
1478
	$salt = defined('YOURLS_COOKIEKEY') ? YOURLS_COOKIEKEY : md5(__FILE__) ;
1479
	return yourls_apply_filter( 'yourls_salt', md5 ($string . $salt), $string );
1480
}
1481
1482
/**
1483
 * Add a query var to a URL and return URL. Completely stolen from WP.
1484
 *
1485
 * Works with one of these parameter patterns:
1486
 *     array( 'var' => 'value' )
1487
 *     array( 'var' => 'value' ), $url
1488
 *     'var', 'value'
1489
 *     'var', 'value', $url
1490
 * If $url omitted, uses $_SERVER['REQUEST_URI']
1491
 *
1492
 * The result of this function call is a URL : it should be escaped before being printed as HTML
1493
 *
1494
 * @since 1.5
1495
 * @param string|array $param1 Either newkey or an associative_array.
1496
 * @param string       $param2 Either newvalue or oldquery or URI.
1497
 * @param string       $param3 Optional. Old query or URI.
1498
 * @return string New URL query string.
1499
 */
1500
function yourls_add_query_arg() {
1501
	$ret = '';
1502
	if ( is_array( func_get_arg(0) ) ) {
1503 View Code Duplication
		if ( @func_num_args() < 2 || false === @func_get_arg( 1 ) )
1504
			$uri = $_SERVER['REQUEST_URI'];
1505
		else
1506
			$uri = @func_get_arg( 1 );
1507 View Code Duplication
	} else {
1508
		if ( @func_num_args() < 3 || false === @func_get_arg( 2 ) )
1509
			$uri = $_SERVER['REQUEST_URI'];
1510
		else
1511
			$uri = @func_get_arg( 2 );
1512
	}
1513
1514
	$uri = str_replace( '&amp;', '&', $uri );
1515
1516
1517
	if ( $frag = strstr( $uri, '#' ) )
1518
		$uri = substr( $uri, 0, -strlen( $frag ) );
1519
	else
1520
		$frag = '';
1521
1522
	if ( preg_match( '|^https?://|i', $uri, $matches ) ) {
1523
		$protocol = $matches[0];
1524
		$uri = substr( $uri, strlen( $protocol ) );
1525
	} else {
1526
		$protocol = '';
1527
	}
1528
1529
	if ( strpos( $uri, '?' ) !== false ) {
1530
		$parts = explode( '?', $uri, 2 );
1531
		if ( 1 == count( $parts ) ) {
1532
			$base = '?';
1533
			$query = $parts[0];
1534
		} else {
1535
			$base = $parts[0] . '?';
1536
			$query = $parts[1];
1537
		}
1538
	} elseif ( !empty( $protocol ) || strpos( $uri, '=' ) === false ) {
1539
		$base = $uri . '?';
1540
		$query = '';
1541
	} else {
1542
		$base = '';
1543
		$query = $uri;
1544
	}
1545
1546
	parse_str( $query, $qs );
1547
	$qs = yourls_urlencode_deep( $qs ); // this re-URL-encodes things that were already in the query string
1548
	if ( is_array( func_get_arg( 0 ) ) ) {
1549
		$kayvees = func_get_arg( 0 );
1550
		$qs = array_merge( $qs, $kayvees );
1551
	} else {
1552
		$qs[func_get_arg( 0 )] = func_get_arg( 1 );
1553
	}
1554
1555
	foreach ( (array) $qs as $k => $v ) {
1556
		if ( $v === false )
1557
			unset( $qs[$k] );
1558
	}
1559
1560
	$ret = http_build_query( $qs );
1561
	$ret = trim( $ret, '?' );
1562
	$ret = preg_replace( '#=(&|$)#', '$1', $ret );
1563
	$ret = $protocol . $base . $ret . $frag;
1564
	$ret = rtrim( $ret, '?' );
1565
	return $ret;
1566
}
1567
1568
/**
1569
 * Navigates through an array and encodes the values to be used in a URL. Stolen from WP, used in yourls_add_query_arg()
1570
 *
1571
 */
1572
function yourls_urlencode_deep( $value ) {
1573
	$value = is_array( $value ) ? array_map( 'yourls_urlencode_deep', $value ) : urlencode( $value );
1574
	return $value;
1575
}
1576
1577
/**
1578
 * Remove arg from query. Opposite of yourls_add_query_arg. Stolen from WP.
1579
 *
1580
 * The result of this function call is a URL : it should be escaped before being printed as HTML
1581
 *
1582
 * @since 1.5
1583
 * @param string|array $key   Query key or keys to remove.
1584
 * @param bool|string  $query Optional. When false uses the $_SERVER value. Default false.
1585
 * @return string New URL query string.
1586
 */
1587
function yourls_remove_query_arg( $key, $query = false ) {
1588
	if ( is_array( $key ) ) { // removing multiple keys
1589
		foreach ( $key as $k )
1590
			$query = yourls_add_query_arg( $k, false, $query );
1591
		return $query;
1592
	}
1593
	return yourls_add_query_arg( $key, false, $query );
1594
}
1595
1596
/**
1597
 * Return a time-dependent string for nonce creation
1598
 *
1599
 */
1600
function yourls_tick() {
1601
	return ceil( time() / YOURLS_NONCE_LIFE );
1602
}
1603
1604
/**
1605
 * Create a time limited, action limited and user limited token
1606
 *
1607
 */
1608
function yourls_create_nonce( $action, $user = false ) {
1609
	if( false == $user )
1610
		$user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
1611
	$tick = yourls_tick();
1612
	return substr( yourls_salt($tick . $action . $user), 0, 10 );
1613
}
1614
1615
/**
1616
 * Create a nonce field for inclusion into a form
1617
 *
1618
 */
1619
function yourls_nonce_field( $action, $name = 'nonce', $user = false, $echo = true ) {
1620
	$field = '<input type="hidden" id="'.$name.'" name="'.$name.'" value="'.yourls_create_nonce( $action, $user ).'" />';
1621
	if( $echo )
1622
		echo $field."\n";
1623
	return $field;
1624
}
1625
1626
/**
1627
 * Add a nonce to a URL. If URL omitted, adds nonce to current URL
1628
 *
1629
 */
1630
function yourls_nonce_url( $action, $url = false, $name = 'nonce', $user = false ) {
1631
	$nonce = yourls_create_nonce( $action, $user );
1632
	return yourls_add_query_arg( $name, $nonce, $url );
1633
}
1634
1635
/**
1636
 * Check validity of a nonce (ie time span, user and action match).
1637
 *
1638
 * Returns true if valid, dies otherwise (yourls_die() or die($return) if defined)
1639
 * if $nonce is false or unspecified, it will use $_REQUEST['nonce']
1640
 *
1641
 */
1642
function yourls_verify_nonce( $action, $nonce = false, $user = false, $return = '' ) {
1643
	// get user
1644
	if( false == $user )
1645
		$user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
1646
1647
	// get current nonce value
1648
	if( false == $nonce && isset( $_REQUEST['nonce'] ) )
1649
		$nonce = $_REQUEST['nonce'];
1650
1651
	// what nonce should be
1652
	$valid = yourls_create_nonce( $action, $user );
1653
1654
	if( $nonce == $valid ) {
1655
		return true;
1656
	} else {
1657
		if( $return )
1658
			die( $return );
1659
		yourls_die( yourls__( 'Unauthorized action or expired link' ), yourls__( 'Error' ), 403 );
1660
	}
1661
}
1662
1663
/**
1664
 * Converts keyword into short link (prepend with YOURLS base URL)
1665
 *
1666
 */
1667
function yourls_link( $keyword = '' ) {
1668
	$link = YOURLS_SITE . '/' . yourls_sanitize_keyword( $keyword );
1669
	return yourls_apply_filter( 'yourls_link', $link, $keyword );
1670
}
1671
1672
/**
1673
 * Converts keyword into stat link (prepend with YOURLS base URL, append +)
1674
 *
1675
 */
1676
function yourls_statlink( $keyword = '' ) {
1677
	$link = YOURLS_SITE . '/' . yourls_sanitize_keyword( $keyword ) . '+';
1678
	if( yourls_is_ssl() )
1679
        $link = yourls_set_url_scheme( $link, 'https' );
1680
	return yourls_apply_filter( 'yourls_statlink', $link, $keyword );
1681
}
1682
1683
/**
1684
 * Check if we're in API mode. Returns bool
1685
 *
1686
 */
1687
function yourls_is_API() {
1688
    $return = defined( 'YOURLS_API' ) && YOURLS_API == true;
1689
    return yourls_apply_filter( 'is_API', $return );
1690
}
1691
1692
/**
1693
 * Check if we're in Ajax mode. Returns bool
1694
 *
1695
 */
1696
function yourls_is_Ajax() {
1697
    $return = defined( 'YOURLS_AJAX' ) && YOURLS_AJAX == true;
1698
    return yourls_apply_filter( 'is_Ajax', $return );
1699
}
1700
1701
/**
1702
 * Check if we're in GO mode (yourls-go.php). Returns bool
1703
 *
1704
 */
1705
function yourls_is_GO() {
1706
    $return = defined( 'YOURLS_GO' ) && YOURLS_GO == true;
1707
    return yourls_apply_filter( 'is_GO', $return );
1708
}
1709
1710
/**
1711
 * Check if we're displaying stats infos (yourls-infos.php). Returns bool
1712
 *
1713
 */
1714
function yourls_is_infos() {
1715
    $return = defined( 'YOURLS_INFOS' ) && YOURLS_INFOS == true;
1716
    return yourls_apply_filter( 'is_infos', $return );
1717
}
1718
1719
/**
1720
 * Check if we're in the admin area. Returns bool
1721
 *
1722
 */
1723
function yourls_is_admin() {
1724
    $return = defined( 'YOURLS_ADMIN' ) && YOURLS_ADMIN == true;
1725
    return yourls_apply_filter( 'is_admin', $return );
1726
}
1727
1728
/**
1729
 * Check if the server seems to be running on Windows. Not exactly sure how reliable this is.
1730
 *
1731
 */
1732
function yourls_is_windows() {
1733
	return defined( 'DIRECTORY_SEPARATOR' ) && DIRECTORY_SEPARATOR == '\\';
1734
}
1735
1736
/**
1737
 * Check if SSL is required. Returns bool.
1738
 *
1739
 */
1740
function yourls_needs_ssl() {
1741
    $return = defined('YOURLS_ADMIN_SSL') && YOURLS_ADMIN_SSL == true;
1742
    return yourls_apply_filter( 'needs_ssl', $return );
1743
}
1744
1745
/**
1746
 * Return admin link, with SSL preference if applicable.
1747
 *
1748
 */
1749
function yourls_admin_url( $page = '' ) {
1750
	$admin = YOURLS_SITE . '/admin/' . $page;
1751
	if( yourls_is_ssl() or yourls_needs_ssl() ) {
1752
        $admin = yourls_set_url_scheme( $admin, 'https' );
1753
    }
1754
	return yourls_apply_filter( 'admin_url', $admin, $page );
1755
}
1756
1757
/**
1758
 * Return YOURLS_SITE or URL under YOURLS setup, with SSL preference
1759
 *
1760
 */
1761
function yourls_site_url( $echo = true, $url = '' ) {
1762
	$url = yourls_get_relative_url( $url );
1763
	$url = trim( YOURLS_SITE . '/' . $url, '/' );
1764
1765
	// Do not enforce (checking yourls_need_ssl() ) but check current usage so it won't force SSL on non-admin pages
1766
	if( yourls_is_ssl() ) {
1767
		$url = yourls_set_url_scheme( $url, 'https' );
1768
    }
1769
	$url = yourls_apply_filter( 'site_url', $url );
1770
	if( $echo ) {
1771
		echo $url;
1772
    }
1773
	return $url;
1774
}
1775
1776
/**
1777
 * Check if SSL is used, returns bool. Stolen from WP.
1778
 *
1779
 */
1780
function yourls_is_ssl() {
1781
	$is_ssl = false;
1782
	if ( isset( $_SERVER['HTTPS'] ) ) {
1783
		if ( 'on' == strtolower( $_SERVER['HTTPS'] ) )
1784
			$is_ssl = true;
1785
		if ( '1' == $_SERVER['HTTPS'] )
1786
			$is_ssl = true;
1787
	} elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
1788
		$is_ssl = true;
1789
	}
1790
	return yourls_apply_filter( 'is_ssl', $is_ssl );
1791
}
1792
1793
/**
1794
 * Get a remote page title
1795
 *
1796
 * This function returns a string: either the page title as defined in HTML, or the URL if not found
1797
 * The function tries to convert funky characters found in titles to UTF8, from the detected charset.
1798
 * Charset in use is guessed from HTML meta tag, or if not found, from server's 'content-type' response.
1799
 *
1800
 * @param string $url URL
1801
 * @return string Title (sanitized) or the URL if no title found
1802
 */
1803
function yourls_get_remote_title( $url ) {
1804
	// Allow plugins to short-circuit the whole function
1805
	$pre = yourls_apply_filter( 'shunt_get_remote_title', false, $url );
1806
	if ( false !== $pre )
1807
		return $pre;
1808
1809
	$url = yourls_sanitize_url( $url );
1810
1811
	// Only deal with http(s)://
1812
	if( !in_array( yourls_get_protocol( $url ), array( 'http://', 'https://' ) ) )
1813
		return $url;
1814
1815
	$title = $charset = false;
1816
1817
    $max_bytes = yourls_apply_filter( 'get_remote_title_max_byte', 32768 ); // limit data fetching to 32K in order to find a <title> tag
1818
1819
	$response = yourls_http_get( $url, array(), array(), array( 'max_bytes' => $max_bytes ) ); // can be a Request object or an error string
1820
	if( is_string( $response ) ) {
1821
		return $url;
1822
	}
1823
1824
	// Page content. No content? Return the URL
1825
	$content = $response->body;
1826
	if( !$content )
1827
		return $url;
1828
1829
	// look for <title>. No title found? Return the URL
1830
	if ( preg_match('/<title>(.*?)<\/title>/is', $content, $found ) ) {
1831
		$title = $found[1];
1832
		unset( $found );
1833
	}
1834
	if( !$title )
1835
		return $url;
1836
1837
	// Now we have a title. We'll try to get proper utf8 from it.
1838
1839
	// Get charset as (and if) defined by the HTML meta tag. We should match
1840
	// <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
1841
	// or <meta charset='utf-8'> and all possible variations: see https://gist.github.com/ozh/7951236
1842
	if ( preg_match( '/<meta[^>]*charset\s*=["\' ]*([a-zA-Z0-9\-_]+)/is', $content, $found ) ) {
1843
		$charset = $found[1];
1844
		unset( $found );
1845
	} else {
1846
		// No charset found in HTML. Get charset as (and if) defined by the server response
1847
		$_charset = current( $response->headers->getValues( 'content-type' ) );
1848
		if( preg_match( '/charset=(\S+)/', $_charset, $found ) ) {
1849
			$charset = trim( $found[1], ';' );
1850
			unset( $found );
1851
		}
1852
	}
1853
1854
	// Conversion to utf-8 if what we have is not utf8 already
1855
	if( strtolower( $charset ) != 'utf-8' && function_exists( 'mb_convert_encoding' ) ) {
1856
		// We use @ to remove warnings because mb_ functions are easily bitching about illegal chars
1857
		if( $charset ) {
1858
			$title = @mb_convert_encoding( $title, 'UTF-8', $charset );
1859
		} else {
1860
			$title = @mb_convert_encoding( $title, 'UTF-8' );
1861
		}
1862
	}
1863
1864
	// Remove HTML entities
1865
	$title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' );
1866
1867
	// Strip out evil things
1868
	$title = yourls_sanitize_title( $title, $url );
1869
1870
	return yourls_apply_filter( 'get_remote_title', $title, $url );
1871
}
1872
1873
/**
1874
 * Quick UA check for mobile devices. Return boolean.
1875
 *
1876
 */
1877
function yourls_is_mobile_device() {
1878
	// Strings searched
1879
	$mobiles = array(
1880
		'android', 'blackberry', 'blazer',
1881
		'compal', 'elaine', 'fennec', 'hiptop',
1882
		'iemobile', 'iphone', 'ipod', 'ipad',
1883
		'iris', 'kindle', 'opera mobi', 'opera mini',
1884
		'palm', 'phone', 'pocket', 'psp', 'symbian',
1885
		'treo', 'wap', 'windows ce', 'windows phone'
1886
	);
1887
1888
	// Current user-agent
1889
	$current = strtolower( $_SERVER['HTTP_USER_AGENT'] );
1890
1891
	// Check and return
1892
	$is_mobile = ( str_replace( $mobiles, '', $current ) != $current );
1893
	return yourls_apply_filter( 'is_mobile_device', $is_mobile );
1894
}
1895
1896
/**
1897
 * Get request in YOURLS base (eg in 'http://sho.rt/yourls/abcd' get 'abdc')
1898
 *
1899
 * With no parameter passed, this function will guess current page and consider
1900
 * it is the current page requested.
1901
 * For testing purposes, parameters can be passed.
1902
 *
1903
 * @since 1.5
1904
 * @param string $yourls_site   Optional, YOURLS installation URL (default to constant YOURLS_SITE)
1905
 * @param string $uri           Optional, page requested (default to $_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] eg 'sho.rt/yourls/abcd' )
1906
 * @return string               request relative to YOURLS base (eg 'abdc')
1907
 */
1908
function yourls_get_request($yourls_site = false, $uri = false) {
1909
	// Allow plugins to short-circuit the whole function
1910
	$pre = yourls_apply_filter( 'shunt_get_request', false );
1911
	if ( false !== $pre )
1912
		return $pre;
1913
1914
	yourls_do_action( 'pre_get_request', $yourls_site, $uri );
1915
1916
    // Default values
1917
    if (false === $yourls_site) {
1918
        $yourls_site = YOURLS_SITE;
1919
    }
1920
    if (false === $uri) {
1921
        $uri = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
1922
    }
1923
1924
    // Even though the config sample states YOURLS_SITE should be set without trailing slash...
1925
    $yourls_site = rtrim($yourls_site,'/');
1926
1927
    // Ignore protocol & www. prefix
1928
	$root = str_replace( array( 'https://www.', 'http://www.', 'https://', 'http://'  ), '', $yourls_site );
1929
	// Case insensitive comparison of the YOURLS root with the requested URL, to match http://Sho.rt/blah, http://sho.rt/blah, http://www.Sho.rt/blah ...
1930
	$request = preg_replace( "!(?:www\.)?$root/!i", '', $uri, 1 );
1931
1932
	// Unless request looks like a full URL (ie request is a simple keyword) strip query string
1933
	if( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) {
1934
		$request = current( explode( '?', $request ) );
1935
	}
1936
1937
	return yourls_apply_filter( 'get_request', $request );
1938
}
1939
1940
/**
1941
 * Change protocol to match current scheme used (http or https)
1942
 *
1943
 */
1944
function yourls_match_current_protocol( $url, $normal = 'http://', $ssl = 'https://' ) {
1945
	if( yourls_is_ssl() )
1946
		$url = str_replace( $normal, $ssl, $url );
1947
	return yourls_apply_filter( 'match_current_protocol', $url );
1948
}
1949
1950
/**
1951
 * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP.
1952
 *
1953
 */
1954
function yourls_fix_request_uri() {
1955
1956
	$default_server_values = array(
1957
		'SERVER_SOFTWARE' => '',
1958
		'REQUEST_URI' => '',
1959
	);
1960
	$_SERVER = array_merge( $default_server_values, $_SERVER );
1961
1962
	// Fix for IIS when running with PHP ISAPI
1963
	if ( empty( $_SERVER['REQUEST_URI'] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER['SERVER_SOFTWARE'] ) ) ) {
1964
1965
		// IIS Mod-Rewrite
1966
		if ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] ) ) {
1967
			$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
1968
		}
1969
		// IIS Isapi_Rewrite
1970
		else if ( isset( $_SERVER['HTTP_X_REWRITE_URL'] ) ) {
1971
			$_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_REWRITE_URL'];
1972
		} else {
1973
			// Use ORIG_PATH_INFO if there is no PATH_INFO
1974
			if ( !isset( $_SERVER['PATH_INFO'] ) && isset( $_SERVER['ORIG_PATH_INFO'] ) )
1975
				$_SERVER['PATH_INFO'] = $_SERVER['ORIG_PATH_INFO'];
1976
1977
			// Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice)
1978
			if ( isset( $_SERVER['PATH_INFO'] ) ) {
1979
				if ( $_SERVER['PATH_INFO'] == $_SERVER['SCRIPT_NAME'] )
1980
					$_SERVER['REQUEST_URI'] = $_SERVER['PATH_INFO'];
1981
				else
1982
					$_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] . $_SERVER['PATH_INFO'];
1983
			}
1984
1985
			// Append the query string if it exists and isn't null
1986
			if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
1987
				$_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
1988
			}
1989
		}
1990
	}
1991
}
1992
1993
/**
1994
 * Shutdown function, runs just before PHP shuts down execution. Stolen from WP
1995
 *
1996
 */
1997
function yourls_shutdown() {
1998
	yourls_do_action( 'shutdown' );
1999
}
2000
2001
/**
2002
 * Auto detect custom favicon in /user directory, fallback to YOURLS favicon, and echo/return its URL
2003
 *
2004
 */
2005
function yourls_favicon( $echo = true ) {
2006
	static $favicon = null;
2007
2008
	if( $favicon !== null ) {
2009
        if( $echo ) {
2010
            echo $favicon;
2011
        }
2012
        return $favicon;
2013
    }
2014
2015
	$custom = null;
2016
	// search for favicon.(gif|ico|png|jpg|svg)
2017
	foreach( array( 'gif', 'ico', 'png', 'jpg', 'svg' ) as $ext ) {
2018
		if( file_exists( YOURLS_USERDIR. '/favicon.' . $ext ) ) {
2019
			$custom = 'favicon.' . $ext;
2020
			break;
2021
		}
2022
	}
2023
2024
	if( $custom ) {
2025
		$favicon = yourls_site_url( false, YOURLS_USERURL . '/' . $custom );
2026
	} else {
2027
		$favicon = yourls_site_url( false ) . '/images/favicon.gif';
2028
	}
2029
2030
	if( $echo ) {
2031
		echo $favicon;
2032
    }
2033
	return $favicon;
2034
}
2035
2036
/**
2037
 * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP.
2038
 *
2039
 */
2040
function yourls_check_maintenance_mode() {
2041
2042
	$file = YOURLS_ABSPATH . '/.maintenance' ;
2043
	if ( !file_exists( $file ) || yourls_is_upgrading() || yourls_is_installing() )
2044
		return;
2045
2046
	global $maintenance_start;
2047
2048
	include_once( $file );
2049
	// If the $maintenance_start timestamp is older than 10 minutes, don't die.
2050
	if ( ( time() - $maintenance_start ) >= 600 )
2051
		return;
2052
2053
	// Use any /user/maintenance.php file
2054
	if( file_exists( YOURLS_USERDIR.'/maintenance.php' ) ) {
2055
		include_once( YOURLS_USERDIR.'/maintenance.php' );
2056
		die();
2057
	}
2058
2059
	// https://www.youtube.com/watch?v=Xw-m4jEY-Ns
2060
	$title   = yourls__( 'Service temporarily unavailable' );
2061
	$message = yourls__( 'Our service is currently undergoing scheduled maintenance.' ) . "</p>\n<p>" .
2062
	yourls__( 'Things should not last very long, thank you for your patience and please excuse the inconvenience' );
2063
	yourls_die( $message, $title , 503 );
2064
2065
}
2066
2067
/**
2068
 * Return current admin page, or null if not an admin page
2069
 *
2070
 * @return mixed string if admin page, null if not an admin page
2071
 * @since 1.6
2072
 */
2073
function yourls_current_admin_page() {
2074
	if( yourls_is_admin() ) {
2075
		$current = substr( yourls_get_request(), 6 );
2076
		if( $current === false )
2077
			$current = 'index.php'; // if current page is http://sho.rt/admin/ instead of http://sho.rt/admin/index.php
2078
2079
		return $current;
2080
	}
2081
	return null;
2082
}
2083
2084
/**
2085
 * Check if a URL protocol is allowed
2086
 *
2087
 * Checks a URL against a list of whitelisted protocols. Protocols must be defined with
2088
 * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid
2089
 * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either
2090
 *
2091
 * @since 1.6
2092
 * @see yourls_get_protocol()
2093
 *
2094
 * @param string $url URL to be check
2095
 * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols
2096
 * @return boolean true if protocol allowed, false otherwise
2097
 */
2098
function yourls_is_allowed_protocol( $url, $protocols = array() ) {
2099
	if( ! $protocols ) {
2100
		global $yourls_allowedprotocols;
2101
		$protocols = $yourls_allowedprotocols;
2102
	}
2103
2104
	$protocol = yourls_get_protocol( $url );
2105
	return yourls_apply_filter( 'is_allowed_protocol', in_array( $protocol, $protocols ), $url, $protocols );
2106
}
2107
2108
/**
2109
 * Get protocol from a URL (eg mailto:, http:// ...)
2110
 *
2111
 * What we liberally call a "protocol" in YOURLS is the scheme name + colon + double slashes if present of a URI. Examples:
2112
 * "something://blah" -> "something://"
2113
 * "something:blah"   -> "something:"
2114
 * "something:/blah"  -> "something:"
2115
 *
2116
 * Unit Tests for this function are located in tests/format/urls.php
2117
 *
2118
 * @since 1.6
2119
 *
2120
 * @param string $url URL to be check
2121
 * @return string Protocol, with slash slash if applicable. Empty string if no protocol
2122
 */
2123
function yourls_get_protocol( $url ) {
2124
	preg_match( '!^[a-zA-Z][a-zA-Z0-9\+\.-]+:(//)?!', $url, $matches );
2125
	/*
2126
	http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
2127
	The scheme name consists of a sequence of characters beginning with a letter and followed by any
2128
	combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are
2129
	case-insensitive, the canonical form is lowercase and documents that specify schemes must do so
2130
	with lowercase letters. It is followed by a colon (":").
2131
	*/
2132
	$protocol = ( isset( $matches[0] ) ? $matches[0] : '' );
2133
	return yourls_apply_filter( 'get_protocol', $protocol, $url );
2134
}
2135
2136
/**
2137
 * Get relative URL (eg 'abc' from 'http://sho.rt/abc')
2138
 *
2139
 * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is
2140
 * or return empty string if $strict is true
2141
 *
2142
 * @since 1.6
2143
 * @param string $url URL to relativize
2144
 * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string
2145
 * @return string URL
2146
 */
2147
function yourls_get_relative_url( $url, $strict = true ) {
2148
	$url = yourls_sanitize_url( $url );
2149
2150
	// Remove protocols to make it easier
2151
	$noproto_url  = str_replace( 'https:', 'http:', $url );
2152
	$noproto_site = str_replace( 'https:', 'http:', YOURLS_SITE );
2153
2154
	// Trim URL from YOURLS root URL : if no modification made, URL wasn't relative
2155
	$_url = str_replace( $noproto_site . '/', '', $noproto_url );
2156
	if( $_url == $noproto_url )
2157
		$_url = ( $strict ? '' : $url );
2158
2159
	return yourls_apply_filter( 'get_relative_url', $_url, $url );
2160
}
2161
2162
/**
2163
 * Marks a function as deprecated and informs when it has been used. Stolen from WP.
2164
 *
2165
 * There is a hook deprecated_function that will be called that can be used
2166
 * to get the backtrace up to what file and function called the deprecated
2167
 * function.
2168
 *
2169
 * The current behavior is to trigger a user error if YOURLS_DEBUG is true.
2170
 *
2171
 * This function is to be used in every function that is deprecated.
2172
 *
2173
 * @since 1.6
2174
 * @uses yourls_do_action() Calls 'deprecated_function' and passes the function name, what to use instead,
2175
 *   and the version the function was deprecated in.
2176
 * @uses yourls_apply_filter() Calls 'deprecated_function_trigger_error' and expects boolean value of true to do
2177
 *   trigger or false to not trigger error.
2178
 *
2179
 * @param string $function The function that was called
2180
 * @param string $version The version of WordPress that deprecated the function
2181
 * @param string $replacement Optional. The function that should have been called
2182
 */
2183
function yourls_deprecated_function( $function, $version, $replacement = null ) {
2184
2185
	yourls_do_action( 'deprecated_function', $function, $replacement, $version );
2186
2187
	// Allow plugin to filter the output error trigger
2188
	if ( YOURLS_DEBUG && yourls_apply_filter( 'deprecated_function_trigger_error', true ) ) {
2189
		if ( ! is_null( $replacement ) )
2190
			trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) );
2191
		else
2192
			trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.'), $function, $version ) );
2193
	}
2194
}
2195
2196
/**
2197
 * Return the value if not an empty string
2198
 *
2199
 * Used with array_filter(), to remove empty keys but not keys with value 0 or false
2200
 *
2201
 * @since 1.6
2202
 * @param mixed $val Value to test against ''
2203
 * @return bool True if not an empty string
2204
 */
2205
function yourls_return_if_not_empty_string( $val ) {
2206
	return( $val !== '' );
2207
}
2208
2209
/**
2210
 * Returns true.
2211
 *
2212
 * Useful for returning true to filters easily.
2213
 *
2214
 * @since 1.7.1
2215
 * @return bool True.
2216
 */
2217
function yourls_return_true() {
2218
    return true;
2219
}
2220
2221
/**
2222
 * Returns false.
2223
 *
2224
 * Useful for returning false to filters easily.
2225
 *
2226
 * @since 1.7.1
2227
 * @return bool False.
2228
 */
2229
function yourls_return_false() {
2230
    return false;
2231
}
2232
2233
/**
2234
 * Returns 0.
2235
 *
2236
 * Useful for returning 0 to filters easily.
2237
 *
2238
 * @since 1.7.1
2239
 * @return int 0.
2240
 */
2241
function yourls_return_zero() {
2242
    return 0;
2243
}
2244
2245
/**
2246
 * Returns an empty array.
2247
 *
2248
 * Useful for returning an empty array to filters easily.
2249
 *
2250
 * @since 1.7.1
2251
 * @return array Empty array.
2252
 */
2253
function yourls_return_empty_array() {
2254
    return array();
2255
}
2256
2257
/**
2258
 * Returns null.
2259
 *
2260
 * Useful for returning null to filters easily.
2261
 *
2262
 * @since 1.7.1
2263
 * @return null Null value.
2264
 */
2265
function yourls_return_null() {
2266
    return null;
2267
}
2268
2269
/**
2270
 * Returns an empty string.
2271
 *
2272
 * Useful for returning an empty string to filters easily.
2273
 *
2274
 * @since 1.7.1
2275
 * @return string Empty string.
2276
 */
2277
function yourls_return_empty_string() {
2278
    return '';
2279
}
2280
2281
/**
2282
 * Add a message to the debug log
2283
 *
2284
 * When in debug mode ( YOURLS_DEBUG == true ) the debug log is echoed in yourls_html_footer()
2285
 * Log messages are appended to $ydb->debug_log array, which is instanciated within class ezSQLcore_YOURLS
2286
 *
2287
 * @since 1.7
2288
 * @param string $msg Message to add to the debug log
2289
 * @return string The message itself
2290
 */
2291
function yourls_debug_log( $msg ) {
2292
	global $ydb;
2293
	$ydb->debug_log[] = $msg;
2294
	return $msg;
2295
}
2296
2297
/**
2298
 * Explode a URL in an array of ( 'protocol' , 'slashes if any', 'rest of the URL' )
2299
 *
2300
 * Some hosts trip up when a query string contains 'http://' - see http://git.io/j1FlJg
2301
 * The idea is that instead of passing the whole URL to a bookmarklet, eg index.php?u=http://blah.com,
2302
 * we pass it by pieces to fool the server, eg index.php?proto=http:&slashes=//&rest=blah.com
2303
 *
2304
 * Known limitation: this won't work if the rest of the URL itself contains 'http://', for example
2305
 * if rest = blah.com/file.php?url=http://foo.com
2306
 *
2307
 * Sample returns:
2308
 *
2309
 *   with 'mailto:[email protected]?subject=hey' :
2310
 *   array( 'protocol' => 'mailto:', 'slashes' => '', 'rest' => '[email protected]?subject=hey' )
2311
 *
2312
 *   with 'http://example.com/blah.html' :
2313
 *   array( 'protocol' => 'http:', 'slashes' => '//', 'rest' => 'example.com/blah.html' )
2314
 *
2315
 * @since 1.7
2316
 * @param string $url URL to be parsed
2317
 * @param array $array Optional, array of key names to be used in returned array
2318
 * @return mixed false if no protocol found, array of ('protocol' , 'slashes', 'rest') otherwise
2319
 */
2320
function yourls_get_protocol_slashes_and_rest( $url, $array = array( 'protocol', 'slashes', 'rest' ) ) {
2321
	$proto = yourls_get_protocol( $url );
2322
2323
	if( !$proto or count( $array ) != 3 )
2324
		return false;
2325
2326
	list( $null, $rest ) = explode( $proto, $url, 2 );
2327
2328
	list( $proto, $slashes ) = explode( ':', $proto );
2329
2330
	return array( $array[0] => $proto . ':', $array[1] => $slashes, $array[2] => $rest );
2331
}
2332
2333
/**
2334
 * Set URL scheme (to HTTP or HTTPS)
2335
 *
2336
 * @since 1.7.1
2337
 * @param string $url URL
2338
 * @param string $scheme scheme, either 'http' or 'https'
2339
 * @return string URL with chosen scheme
2340
 */
2341
function yourls_set_url_scheme( $url, $scheme = false ) {
2342
    if( $scheme != 'http' && $scheme != 'https' ) {
2343
        return $url;
2344
    }
2345
    return preg_replace( '!^[a-zA-Z0-9\+\.-]+://!', $scheme . '://', $url );
2346
}
2347
2348
/**
2349
 * Tell if there is a new YOURLS version
2350
 *
2351
 * This function checks, if needed, if there's a new version of YOURLS and, if applicable, display
2352
 * an update notice.
2353
 *
2354
 * @since 1.7.3
2355
 */
2356
function yourls_tell_if_new_version() {
2357
    $check = yourls_maybe_check_core_version();
2358
    yourls_debug_log( 'Check for new version: ' . ($check ? 'yes' : 'no') );
2359
    yourls_new_core_version_notice();
2360
}
2361