]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/functions.php
Fix end of line
[Github/YOURLS.git] / includes / functions.php
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') ) {
17                 $charset = '0123456789abcdefghijklmnopqrstuvwxyz';
18         } else {
19                 switch( YOURLS_URL_CONVERT ) {
20                         case 36:
21                                 $charset = '0123456789abcdefghijklmnopqrstuvwxyz';
22                                 break;
23                         case 62:
24                         case 64: // just because some people get this wrong in their config.php
25                                 $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
26                                 break;
27                 }
28         }
29         
30         $charset = yourls_apply_filter( 'get_shorturl_charset', $charset );
31         return $charset;
32 }
33  
34 /**
35  * Make an optimized regexp pattern from a string of characters
36  * 
37  */
38 function yourls_make_regexp_pattern( $string ) {
39         $pattern = preg_quote( $string, '-' ); // add - as an escaped characters -- this is fixed in PHP 5.3
40         // TODO: replace char sequences by smart sequences such as 0-9, a-z, A-Z ... ?
41         return $pattern;
42 }
43
44 /**
45  * Is a URL a short URL?
46  * 
47  */
48 function yourls_is_shorturl( $shorturl ) {
49         // TODO: make sure this function evolves with the feature set.
50         
51         $is_short = false;
52         $keyword = yourls_get_relative_url( $shorturl ); // accept either 'http://ozh.in/abc' or 'abc'
53         if( $keyword && $keyword == yourls_sanitize_string( $keyword ) && yourls_keyword_is_taken( $keyword ) ) {
54                 $is_short = true;
55         }
56         
57         return yourls_apply_filter( 'is_shorturl', $is_short, $shorturl );
58 }
59
60 /**
61  * Check to see if a given keyword is reserved (ie reserved URL or an existing page). Returns bool
62  *
63  */
64 function yourls_keyword_is_reserved( $keyword ) {
65         global $yourls_reserved_URL;
66         $keyword = yourls_sanitize_keyword( $keyword );
67         $reserved = false;
68         
69         if ( in_array( $keyword, $yourls_reserved_URL)
70                 or file_exists( YOURLS_ABSPATH ."/pages/$keyword.php" )
71                 or is_dir( YOURLS_ABSPATH ."/$keyword" )
72         )
73                 $reserved = true;
74         
75         return yourls_apply_filter( 'keyword_is_reserved', $reserved, $keyword );
76 }
77
78 /**
79  * Function: Get client IP Address. Returns a DB safe string.
80  *
81  */
82 function yourls_get_IP() {
83         // Precedence: if set, X-Forwarded-For > HTTP_X_FORWARDED_FOR > HTTP_CLIENT_IP > HTTP_VIA > REMOTE_ADDR
84         $headers = array( 'X-Forwarded-For', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_VIA', 'REMOTE_ADDR' );
85         foreach( $headers as $header ) {
86                 if ( !empty( $_SERVER[ $header ] ) ) {
87                         $ip = $_SERVER[ $header ];
88                         break;
89                 }
90         }
91         
92         // headers can contain multiple IPs (X-Forwarded-For = client, proxy1, proxy2). Take first one.
93         if ( strpos( $ip, ',' ) !== false )
94                 $ip = substr( $ip, 0, strpos( $ip, ',' ) );
95         
96         return yourls_apply_filter( 'get_IP', yourls_sanitize_ip( $ip ) );
97 }
98
99 /**
100  * Get next id a new link will have if no custom keyword provided
101  *
102  */
103 function yourls_get_next_decimal() {
104         return yourls_apply_filter( 'get_next_decimal', (int)yourls_get_option( 'next_id' ) );
105 }
106
107 /**
108  * Update id for next link with no custom keyword
109  *
110  */
111 function yourls_update_next_decimal( $int = '' ) {
112         $int = ( $int == '' ) ? yourls_get_next_decimal() + 1 : (int)$int ;
113         $update = yourls_update_option( 'next_id', $int );
114         yourls_do_action( 'update_next_decimal', $int, $update );
115         return $update;
116 }
117
118 /**
119  * Delete a link in the DB
120  *
121  */
122 function yourls_delete_link_by_keyword( $keyword ) {
123         global $ydb;
124
125         $table = YOURLS_DB_TABLE_URL;
126         $keyword = yourls_sanitize_string( $keyword );
127         $delete = $ydb->query("DELETE FROM `$table` WHERE `keyword` = '$keyword';");
128         yourls_do_action( 'delete_link', $keyword, $delete );
129         return $delete;
130 }
131
132 /**
133  * SQL query to insert a new link in the DB. Returns boolean for success or failure of the inserting
134  *
135  */
136 function yourls_insert_link_in_db( $url, $keyword, $title = '' ) {
137         global $ydb;
138         
139         $url     = yourls_escape( yourls_sanitize_url( $url ) );
140         $keyword = yourls_escape( yourls_sanitize_keyword( $keyword ) );
141         $title   = yourls_escape( yourls_sanitize_title( $title ) );
142
143         $table = YOURLS_DB_TABLE_URL;
144         $timestamp = date('Y-m-d H:i:s');
145         $ip = yourls_get_IP();
146         $insert = $ydb->query("INSERT INTO `$table` (`keyword`, `url`, `title`, `timestamp`, `ip`, `clicks`) VALUES('$keyword', '$url', '$title', '$timestamp', '$ip', 0);");
147         
148         yourls_do_action( 'insert_link', (bool)$insert, $url, $keyword, $title, $timestamp, $ip );
149         
150         return (bool)$insert;
151 }
152
153 /**
154  * Check if a URL already exists in the DB. Return NULL (doesn't exist) or an object with URL informations.
155  *
156  */
157 function yourls_url_exists( $url ) {
158         // Allow plugins to short-circuit the whole function
159         $pre = yourls_apply_filter( 'shunt_url_exists', false, $url );
160         if ( false !== $pre )
161                 return $pre;
162
163         global $ydb;
164         $table = YOURLS_DB_TABLE_URL;
165         $strip_url = stripslashes($url);
166         $url_exists = $ydb->get_row("SELECT * FROM `$table` WHERE `url` = '".$strip_url."';");
167         
168         return yourls_apply_filter( 'url_exists', $url_exists, $url );
169 }
170
171 /**
172  * Add a new link in the DB, either with custom keyword, or find one
173  *
174  */
175 function yourls_add_new_link( $url, $keyword = '', $title = '' ) {
176         global $ydb;
177
178         // Allow plugins to short-circuit the whole function
179         $pre = yourls_apply_filter( 'shunt_add_new_link', false, $url, $keyword, $title );
180         if ( false !== $pre )
181                 return $pre;
182                 
183         $url = yourls_encodeURI( $url );
184         $url = yourls_escape( yourls_sanitize_url( $url ) );
185         if ( !$url || $url == 'http://' || $url == 'https://' ) {
186                 $return['status']    = 'fail';
187                 $return['code']      = 'error:nourl';
188                 $return['message']   = yourls__( 'Missing or malformed URL' );
189                 $return['errorCode'] = '400';
190                 return yourls_apply_filter( 'add_new_link_fail_nourl', $return, $url, $keyword, $title );
191         }
192         
193         // Prevent DB flood
194         $ip = yourls_get_IP();
195         yourls_check_IP_flood( $ip );
196         
197         // Prevent internal redirection loops: cannot shorten a shortened URL
198         if( yourls_get_relative_url( $url ) ) {
199                 if( yourls_is_shorturl( $url ) ) {
200                         $return['status']    = 'fail';
201                         $return['code']      = 'error:noloop';
202                         $return['message']   = yourls__( 'URL is a short URL' );
203                         $return['errorCode'] = '400';
204                         return yourls_apply_filter( 'add_new_link_fail_noloop', $return, $url, $keyword, $title );
205                 }
206         }
207
208         yourls_do_action( 'pre_add_new_link', $url, $keyword, $title );
209         
210         $strip_url = stripslashes( $url );
211         $return = array();
212
213         // duplicates allowed or new URL => store it
214         if( yourls_allow_duplicate_longurls() || !( $url_exists = yourls_url_exists( $url ) ) ) {
215         
216                 if( isset( $title ) && !empty( $title ) ) {
217                         $title = yourls_sanitize_title( $title );
218                 } else {
219                         $title = yourls_get_remote_title( $url );
220                 }
221                 $title = yourls_apply_filter( 'add_new_title', $title, $url, $keyword );
222
223                 // Custom keyword provided
224                 if ( $keyword ) {
225                         
226                         yourls_do_action( 'add_new_link_custom_keyword', $url, $keyword, $title );
227                 
228                         $keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
229                         $keyword = yourls_apply_filter( 'custom_keyword', $keyword, $url, $title );
230                         if ( !yourls_keyword_is_free( $keyword ) ) {
231                                 // This shorturl either reserved or taken already
232                                 $return['status']  = 'fail';
233                                 $return['code']    = 'error:keyword';
234                                 $return['message'] = yourls_s( 'Short URL %s already exists in database or is reserved', $keyword );
235                         } else {
236                                 // all clear, store !
237                                 yourls_insert_link_in_db( $url, $keyword, $title );
238                                 $return['url']      = array('keyword' => $keyword, 'url' => $strip_url, 'title' => $title, 'date' => date('Y-m-d H:i:s'), 'ip' => $ip );
239                                 $return['status']   = 'success';
240                                 $return['message']  = /* //translators: eg "http://someurl/ added to DB" */ yourls_s( '%s added to database', yourls_trim_long_string( $strip_url ) );
241                                 $return['title']    = $title;
242                                 $return['html']     = yourls_table_add_row( $keyword, $url, $title, $ip, 0, time() );
243                                 $return['shorturl'] = YOURLS_SITE .'/'. $keyword;
244                         }
245
246                 // Create random keyword        
247                 } else {
248                         
249                         yourls_do_action( 'add_new_link_create_keyword', $url, $keyword, $title );
250                 
251                         $timestamp = date( 'Y-m-d H:i:s' );
252                         $id = yourls_get_next_decimal();
253                         $ok = false;
254                         do {
255                                 $keyword = yourls_int2string( $id );
256                                 $keyword = yourls_apply_filter( 'random_keyword', $keyword, $url, $title );
257                                 $free = yourls_keyword_is_free($keyword);
258                                 $add_url = @yourls_insert_link_in_db( $url, $keyword, $title );
259                                 $ok = ($free && $add_url);
260                                 if ( $ok === false && $add_url === 1 ) {
261                                         // we stored something, but shouldn't have (ie reserved id)
262                                         $delete = yourls_delete_link_by_keyword( $keyword );
263                                         $return['extra_info'] .= '(deleted '.$keyword.')';
264                                 } else {
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                                 }
273                                 $id++;
274                         } while ( !$ok );
275                         @yourls_update_next_decimal( $id );
276                 }
277
278         // URL was already stored
279         } else {
280                         
281                 yourls_do_action( 'add_new_link_already_stored', $url, $keyword, $title );
282                 
283                 $return['status']   = 'fail';
284                 $return['code']     = 'error:url';
285                 $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 );
286                 $return['message']  = /* //translators: eg "http://someurl/ already exists" */ yourls_s( '%s already exists in database', yourls_trim_long_string( $strip_url ) );
287                 $return['title']    = $url_exists->title; 
288                 $return['shorturl'] = YOURLS_SITE .'/'. $url_exists->keyword;
289         }
290         
291         yourls_do_action( 'post_add_new_link', $url, $keyword, $title );
292
293         $return['statusCode'] = 200; // regardless of result, this is still a valid request
294         return yourls_apply_filter( 'add_new_link', $return, $url, $keyword, $title );
295 }
296
297
298 /**
299  * Edit a link
300  *
301  */
302 function yourls_edit_link( $url, $keyword, $newkeyword='', $title='' ) {
303         global $ydb;
304
305         $table = YOURLS_DB_TABLE_URL;
306         $url = yourls_escape (yourls_sanitize_url( $url ) );
307         $keyword = yourls_escape( yourls_sanitize_string( $keyword ) );
308         $title = yourls_escape( yourls_sanitize_title( $title ) );
309         $newkeyword = yourls_escape( yourls_sanitize_string( $newkeyword ) );
310         $strip_url = stripslashes( $url );
311         $strip_title = stripslashes( $title );
312         $old_url = $ydb->get_var( "SELECT `url` FROM `$table` WHERE `keyword` = '$keyword';" );
313         
314         // Check if new URL is not here already
315         if ( $old_url != $url && !yourls_allow_duplicate_longurls() ) {
316                 $new_url_already_there = intval($ydb->get_var("SELECT COUNT(keyword) FROM `$table` WHERE `url` = '$strip_url';"));
317         } else {
318                 $new_url_already_there = false;
319         }
320         
321         // Check if the new keyword is not here already
322         if ( $newkeyword != $keyword ) {
323                 $keyword_is_ok = yourls_keyword_is_free( $newkeyword );
324         } else {
325                 $keyword_is_ok = true;
326         }
327         
328         yourls_do_action( 'pre_edit_link', $url, $keyword, $newkeyword, $new_url_already_there, $keyword_is_ok );
329         
330         // All clear, update
331         if ( ( !$new_url_already_there || yourls_allow_duplicate_longurls() ) && $keyword_is_ok ) {
332                         $update_url = $ydb->query( "UPDATE `$table` SET `url` = '$url', `keyword` = '$newkeyword', `title` = '$title' WHERE `keyword` = '$keyword';" );
333                 if( $update_url ) {
334                         $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 ) );
335                         $return['status']  = 'success';
336                         $return['message'] = yourls__( 'Link updated in database' );
337                 } else {
338                         $return['status']  = 'fail';
339                         $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 ) ;
340                 }
341         
342         // Nope
343         } else {
344                 $return['status']  = 'fail';
345                 $return['message'] = yourls__( 'URL or keyword already exists in database' );
346         }
347         
348         return yourls_apply_filter( 'edit_link', $return, $url, $keyword, $newkeyword, $title, $new_url_already_there, $keyword_is_ok );
349 }
350
351 /**
352  * Update a title link (no checks for duplicates etc..)
353  *
354  */
355 function yourls_edit_link_title( $keyword, $title ) {
356         global $ydb;
357         
358         $keyword = yourls_escape( yourls_sanitize_keyword( $keyword ) );
359         $title = yourls_escape( yourls_sanitize_title( $title ) );
360         
361         $table = YOURLS_DB_TABLE_URL;
362         $update = $ydb->query("UPDATE `$table` SET `title` = '$title' WHERE `keyword` = '$keyword';");
363
364         return $update;
365 }
366
367
368 /**
369  * Check if keyword id is free (ie not already taken, and not reserved). Return bool.
370  *
371  */
372 function yourls_keyword_is_free( $keyword ) {
373         $free = true;
374         if ( yourls_keyword_is_reserved( $keyword ) or yourls_keyword_is_taken( $keyword ) )
375                 $free = false;
376                 
377         return yourls_apply_filter( 'keyword_is_free', $free, $keyword );
378 }
379
380 /**
381  * Check if a keyword is taken (ie there is already a short URL with this id). Return bool.             
382  *
383  */
384 function yourls_keyword_is_taken( $keyword ) {
385         global $ydb;
386         $keyword = yourls_sanitize_keyword( $keyword );
387         $taken = false;
388         $table = YOURLS_DB_TABLE_URL;
389         $already_exists = $ydb->get_var( "SELECT COUNT(`keyword`) FROM `$table` WHERE `keyword` = '$keyword';" );
390         if ( $already_exists )
391                 $taken = true;
392
393         return yourls_apply_filter( 'keyword_is_taken', $taken, $keyword );
394 }
395
396
397 /**
398  * Connect to DB
399  *
400  */
401 function yourls_db_connect() {
402         global $ydb;
403
404         if (   !defined( 'YOURLS_DB_USER' )
405                 or !defined( 'YOURLS_DB_PASS' )
406                 or !defined( 'YOURLS_DB_NAME' )
407                 or !defined( 'YOURLS_DB_HOST' )
408                 or !class_exists( 'ezSQL_mysql' )
409         ) yourls_die ( yourls__( 'DB config missing, or could not find DB class' ), yourls__( 'Fatal error' ), 503 );
410         
411         // Are we standalone or in the WordPress environment?
412         if ( class_exists( 'wpdb' ) ) {
413                 $ydb =  new wpdb( YOURLS_DB_USER, YOURLS_DB_PASS, YOURLS_DB_NAME, YOURLS_DB_HOST );
414         } else {
415                 $ydb =  new ezSQL_mysql( YOURLS_DB_USER, YOURLS_DB_PASS, YOURLS_DB_NAME, YOURLS_DB_HOST );
416         }
417         if ( $ydb->last_error )
418                 yourls_die( $ydb->last_error, yourls__( 'Fatal error' ), 503 );
419         
420         if ( defined( 'YOURLS_DEBUG' ) && YOURLS_DEBUG === true )
421                 $ydb->show_errors = true;
422         
423         return $ydb;
424 }
425
426 /**
427  * Return XML output.
428  *
429  */
430 function yourls_xml_encode( $array ) {
431         require_once( YOURLS_INC.'/functions-xml.php' );
432         $converter= new yourls_array2xml;
433         return $converter->array2xml( $array );
434 }
435
436 /**
437  * Return array of all informations associated with keyword. Returns false if keyword not found. Set optional $use_cache to false to force fetching from DB
438  *
439  */
440 function yourls_get_keyword_infos( $keyword, $use_cache = true ) {
441         global $ydb;
442         $keyword = yourls_sanitize_string( $keyword );
443
444         yourls_do_action( 'pre_get_keyword', $keyword, $use_cache );
445
446         if( isset( $ydb->infos[$keyword] ) && $use_cache == true ) {
447                 return yourls_apply_filter( 'get_keyword_infos', $ydb->infos[$keyword], $keyword );
448         }
449         
450         yourls_do_action( 'get_keyword_not_cached', $keyword );
451         
452         $table = YOURLS_DB_TABLE_URL;
453         $infos = $ydb->get_row( "SELECT * FROM `$table` WHERE `keyword` = '$keyword'" );
454         
455         if( $infos ) {
456                 $infos = (array)$infos;
457                 $ydb->infos[ $keyword ] = $infos;
458         } else {
459                 $ydb->infos[ $keyword ] = false;
460         }
461                 
462         return yourls_apply_filter( 'get_keyword_infos', $ydb->infos[$keyword], $keyword );
463 }
464
465 /**
466  * Return (string) selected information associated with a keyword. Optional $notfound = string default message if nothing found
467  *
468  */
469 function yourls_get_keyword_info( $keyword, $field, $notfound = false ) {
470
471         // Allow plugins to short-circuit the whole function
472         $pre = yourls_apply_filter( 'shunt_get_keyword_info', false, $keyword, $field, $notfound );
473         if ( false !== $pre )
474                 return $pre;
475
476         $keyword = yourls_sanitize_string( $keyword );
477         $infos = yourls_get_keyword_infos( $keyword );
478         
479         $return = $notfound;
480         if ( isset( $infos[ $field ] ) && $infos[ $field ] !== false )
481                 $return = $infos[ $field ];
482
483         return yourls_apply_filter( 'get_keyword_info', $return, $keyword, $field, $notfound ); 
484 }
485
486 /**
487  * Return title associated with keyword. Optional $notfound = string default message if nothing found
488  *
489  */
490 function yourls_get_keyword_title( $keyword, $notfound = false ) {
491         return yourls_get_keyword_info( $keyword, 'title', $notfound );
492 }
493
494 /**
495  * Return long URL associated with keyword. Optional $notfound = string default message if nothing found
496  *
497  */
498 function yourls_get_keyword_longurl( $keyword, $notfound = false ) {
499         return yourls_get_keyword_info( $keyword, 'url', $notfound );
500 }
501
502 /**
503  * Return number of clicks on a keyword. Optional $notfound = string default message if nothing found
504  *
505  */
506 function yourls_get_keyword_clicks( $keyword, $notfound = false ) {
507         return yourls_get_keyword_info( $keyword, 'clicks', $notfound );
508 }
509
510 /**
511  * Return IP that added a keyword. Optional $notfound = string default message if nothing found
512  *
513  */
514 function yourls_get_keyword_IP( $keyword, $notfound = false ) {
515         return yourls_get_keyword_info( $keyword, 'ip', $notfound );
516 }
517
518 /**
519  * Return timestamp associated with a keyword. Optional $notfound = string default message if nothing found
520  *
521  */
522 function yourls_get_keyword_timestamp( $keyword, $notfound = false ) {
523         return yourls_get_keyword_info( $keyword, 'timestamp', $notfound );
524 }
525
526 /**
527  * Update click count on a short URL. Return 0/1 for error/success.
528  *
529  */
530 function yourls_update_clicks( $keyword, $clicks = false ) {
531         // Allow plugins to short-circuit the whole function
532         $pre = yourls_apply_filter( 'shunt_update_clicks', false, $keyword, $clicks );
533         if ( false !== $pre )
534                 return $pre;
535
536         global $ydb;
537         $keyword = yourls_sanitize_string( $keyword );
538         $table = YOURLS_DB_TABLE_URL;
539         if ( $clicks !== false && is_int( $clicks ) && $clicks >= 0 )
540                 $update = $ydb->query( "UPDATE `$table` SET `clicks` = $clicks WHERE `keyword` = '$keyword'" );
541         else
542                 $update = $ydb->query( "UPDATE `$table` SET `clicks` = clicks + 1 WHERE `keyword` = '$keyword'" );
543
544         yourls_do_action( 'update_clicks', $keyword, $update, $clicks );
545         return $update;
546 }
547
548 /**
549  * Return array of stats. (string)$filter is 'bottom', 'last', 'rand' or 'top'. (int)$limit is the number of links to return
550  *
551  */
552 function yourls_get_stats( $filter = 'top', $limit = 10, $start = 0 ) {
553         global $ydb;
554
555         switch( $filter ) {
556                 case 'bottom':
557                         $sort_by    = 'clicks';
558                         $sort_order = 'asc';
559                         break;
560                 case 'last':
561                         $sort_by    = 'timestamp';
562                         $sort_order = 'desc';
563                         break;
564                 case 'rand':
565                 case 'random':
566                         $sort_by    = 'RAND()';
567                         $sort_order = '';
568                         break;
569                 case 'top':
570                 default:
571                         $sort_by    = 'clicks';
572                         $sort_order = 'desc';
573                         break;
574         }
575         
576         // Fetch links
577         $limit = intval( $limit );
578         $start = intval( $start );
579         if ( $limit > 0 ) {
580
581                 $table_url = YOURLS_DB_TABLE_URL;
582                 $results = $ydb->get_results( "SELECT * FROM `$table_url` WHERE 1=1 ORDER BY `$sort_by` $sort_order LIMIT $start, $limit;" );
583                 
584                 $return = array();
585                 $i = 1;
586                 
587                 foreach ( (array)$results as $res ) {
588                         $return['links']['link_'.$i++] = array(
589                                 'shorturl' => YOURLS_SITE .'/'. $res->keyword,
590                                 'url'      => $res->url,
591                                 'title'    => $res->title,
592                                 'timestamp'=> $res->timestamp,
593                                 'ip'       => $res->ip,
594                                 'clicks'   => $res->clicks,
595                         );
596                 }
597         }
598
599         $return['stats'] = yourls_get_db_stats();
600         
601         $return['statusCode'] = 200;
602
603         return yourls_apply_filter( 'get_stats', $return, $filter, $limit, $start );
604 }
605
606 /**
607  * Return array of stats. (string)$filter is 'bottom', 'last', 'rand' or 'top'. (int)$limit is the number of links to return
608  *
609  */
610 function yourls_get_link_stats( $shorturl ) {
611         global $ydb;
612
613         $table_url = YOURLS_DB_TABLE_URL;
614         $res = $ydb->get_row( "SELECT * FROM `$table_url` WHERE keyword = '$shorturl';" );
615         $return = array();
616
617         if( !$res ) {
618                 // non existent link
619                 $return = array(
620                         'statusCode' => 404,
621                         'message'    => 'Error: short URL not found',
622                 );
623         } else {
624                 $return = array(
625                         'statusCode' => 200,
626                         'message'    => 'success',
627                         'link'       => array(
628                                 'shorturl' => YOURLS_SITE .'/'. $res->keyword,
629                                 'url'      => $res->url,
630                                 'title'    => $res->title,
631                                 'timestamp'=> $res->timestamp,
632                                 'ip'       => $res->ip,
633                                 'clicks'   => $res->clicks,
634                         )
635                 );
636         }
637
638         return yourls_apply_filter( 'get_link_stats', $return, $shorturl );
639 }
640
641 /**
642  * Get total number of URLs and sum of clicks. Input: optional "AND WHERE" clause. Returns array
643  *
644  */
645 function yourls_get_db_stats( $where = '' ) {
646         global $ydb;
647         $table_url = YOURLS_DB_TABLE_URL;
648
649         $totals = $ydb->get_row( "SELECT COUNT(keyword) as count, SUM(clicks) as sum FROM `$table_url` WHERE 1=1 $where" );
650         $return = array( 'total_links' => $totals->count, 'total_clicks' => $totals->sum );
651         
652         return yourls_apply_filter( 'get_db_stats', $return, $where );
653 }
654
655 /**
656  * Get number of SQL queries performed
657  *
658  */
659 function yourls_get_num_queries() {
660         global $ydb;
661
662         return yourls_apply_filter( 'get_num_queries', $ydb->num_queries );
663 }
664
665 /**
666  * Returns a sanitized a user agent string. Given what I found on http://www.user-agents.org/ it should be OK.
667  *
668  */
669 function yourls_get_user_agent() {
670         if ( !isset( $_SERVER['HTTP_USER_AGENT'] ) )
671                 return '-';
672         
673         $ua = strip_tags( html_entity_decode( $_SERVER['HTTP_USER_AGENT'] ));
674         $ua = preg_replace('![^0-9a-zA-Z\':., /{}\(\)\[\]\+@&\!\?;_\-=~\*\#]!', '', $ua );
675                 
676         return yourls_apply_filter( 'get_user_agent', substr( $ua, 0, 254 ) );
677 }
678
679 /**
680  * Redirect to another page
681  *
682  */
683 function yourls_redirect( $location, $code = 301 ) {
684         yourls_do_action( 'pre_redirect', $location, $code );
685         $location = yourls_apply_filter( 'redirect_location', $location, $code );
686         $code     = yourls_apply_filter( 'redirect_code', $code, $location );
687         // Redirect, either properly if possible, or via Javascript otherwise
688         if( !headers_sent() ) {
689                 yourls_status_header( $code );
690                 header( "Location: $location" );
691         } else {
692                 yourls_redirect_javascript( $location );
693         }
694         die();
695 }
696
697 /**
698  * Set HTTP status header
699  *
700  */
701 function yourls_status_header( $code = 200 ) {
702         if( headers_sent() )
703                 return;
704                 
705         $protocol = $_SERVER["SERVER_PROTOCOL"];
706         if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol )
707                 $protocol = 'HTTP/1.0';
708
709         $code = intval( $code );
710         $desc = yourls_get_HTTP_status( $code );
711
712         @header ("$protocol $code $desc"); // This causes problems on IIS and some FastCGI setups
713         yourls_do_action( 'status_header', $code );
714 }
715
716 /**
717  * 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)
718  *
719  */
720 function yourls_redirect_javascript( $location, $dontwait = true ) {
721         yourls_do_action( 'pre_redirect_javascript', $location, $dontwait );
722         $location = yourls_apply_filter( 'redirect_javascript', $location, $dontwait );
723         if( $dontwait ) {
724                 $message = yourls_s( 'if you are not redirected after 10 seconds, please <a href="%s">click here</a>', $location );
725                 echo <<<REDIR
726                 <script type="text/javascript">
727                 window.location="$location";
728                 </script>
729                 <small>($message)</small>
730 REDIR;
731         } else {
732                 echo '<p>' . yourls_s( 'Please <a href="%s">click here</a>', $location ) . '</p>';
733         }
734         yourls_do_action( 'post_redirect_javascript', $location );
735 }
736
737 /**
738  * Return a HTTP status code
739  *
740  */
741 function yourls_get_HTTP_status( $code ) {
742         $code = intval( $code );
743         $headers_desc = array(
744                 100 => 'Continue',
745                 101 => 'Switching Protocols',
746                 102 => 'Processing',
747
748                 200 => 'OK',
749                 201 => 'Created',
750                 202 => 'Accepted',
751                 203 => 'Non-Authoritative Information',
752                 204 => 'No Content',
753                 205 => 'Reset Content',
754                 206 => 'Partial Content',
755                 207 => 'Multi-Status',
756                 226 => 'IM Used',
757
758                 300 => 'Multiple Choices',
759                 301 => 'Moved Permanently',
760                 302 => 'Found',
761                 303 => 'See Other',
762                 304 => 'Not Modified',
763                 305 => 'Use Proxy',
764                 306 => 'Reserved',
765                 307 => 'Temporary Redirect',
766
767                 400 => 'Bad Request',
768                 401 => 'Unauthorized',
769                 402 => 'Payment Required',
770                 403 => 'Forbidden',
771                 404 => 'Not Found',
772                 405 => 'Method Not Allowed',
773                 406 => 'Not Acceptable',
774                 407 => 'Proxy Authentication Required',
775                 408 => 'Request Timeout',
776                 409 => 'Conflict',
777                 410 => 'Gone',
778                 411 => 'Length Required',
779                 412 => 'Precondition Failed',
780                 413 => 'Request Entity Too Large',
781                 414 => 'Request-URI Too Long',
782                 415 => 'Unsupported Media Type',
783                 416 => 'Requested Range Not Satisfiable',
784                 417 => 'Expectation Failed',
785                 422 => 'Unprocessable Entity',
786                 423 => 'Locked',
787                 424 => 'Failed Dependency',
788                 426 => 'Upgrade Required',
789
790                 500 => 'Internal Server Error',
791                 501 => 'Not Implemented',
792                 502 => 'Bad Gateway',
793                 503 => 'Service Unavailable',
794                 504 => 'Gateway Timeout',
795                 505 => 'HTTP Version Not Supported',
796                 506 => 'Variant Also Negotiates',
797                 507 => 'Insufficient Storage',
798                 510 => 'Not Extended'
799         );
800
801         if ( isset( $headers_desc[$code] ) )
802                 return $headers_desc[$code];
803         else
804                 return '';
805 }
806
807
808 /**
809  * Log a redirect (for stats)
810  *
811  */
812 function yourls_log_redirect( $keyword ) {
813         // Allow plugins to short-circuit the whole function
814         $pre = yourls_apply_filter( 'shunt_log_redirect', false, $keyword );
815         if ( false !== $pre )
816                 return $pre;
817
818         if ( !yourls_do_log_redirect() )
819                 return true;
820
821         global $ydb;
822         $table = YOURLS_DB_TABLE_LOG;
823         
824         $keyword = yourls_sanitize_string( $keyword );
825         $referrer = ( isset( $_SERVER['HTTP_REFERER'] ) ? yourls_sanitize_url( $_SERVER['HTTP_REFERER'] ) : 'direct' );
826         $ua = yourls_get_user_agent();
827         $ip = yourls_get_IP();
828         $location = yourls_geo_ip_to_countrycode( $ip );
829         
830         return $ydb->query( "INSERT INTO `$table` (click_time, shorturl, referrer, user_agent, ip_address, country_code) VALUES (NOW(), '$keyword', '$referrer', '$ua', '$ip', '$location')" );
831 }
832
833 /**
834  * Check if we want to not log redirects (for stats)
835  *
836  */
837 function yourls_do_log_redirect() {
838         return ( !defined( 'YOURLS_NOSTATS' ) || YOURLS_NOSTATS != true );
839 }
840
841 /**
842  * Converts an IP to a 2 letter country code, using GeoIP database if available in includes/geo/
843  *
844  */
845 function yourls_geo_ip_to_countrycode( $ip = '', $default = '' ) {
846         // Allow plugins to short-circuit the Geo IP API
847         $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
848         if ( false !== $location )
849                 return $location;
850
851         if ( !file_exists( YOURLS_INC.'/geo/GeoIP.dat') || !file_exists( YOURLS_INC.'/geo/geoip.inc') )
852                 return $default;
853
854         if ( $ip == '' )
855                 $ip = yourls_get_IP();
856         
857         require_once( YOURLS_INC.'/geo/geoip.inc') ;
858         $gi = geoip_open( YOURLS_INC.'/geo/GeoIP.dat', GEOIP_STANDARD);
859         $location = geoip_country_code_by_addr($gi, $ip);
860         geoip_close($gi);
861
862         return yourls_apply_filter( 'geo_ip_to_countrycode', $location, $ip, $default );
863 }
864
865 /**
866  * Converts a 2 letter country code to long name (ie AU -> Australia)
867  *
868  */
869 function yourls_geo_countrycode_to_countryname( $code ) {
870         // Allow plugins to short-circuit the Geo IP API
871         $country = yourls_apply_filter( 'shunt_geo_countrycode_to_countryname', false, $code );
872         if ( false !== $country )
873                 return $country;
874
875         // Load the Geo class if not already done
876         if( !class_exists( 'GeoIP' ) ) {
877                 $temp = yourls_geo_ip_to_countrycode( '127.0.0.1' );
878         }
879         
880         if( class_exists( 'GeoIP' ) ) {
881                 $geo  = new GeoIP;
882                 $id   = $geo->GEOIP_COUNTRY_CODE_TO_NUMBER[ $code ];
883                 $long = $geo->GEOIP_COUNTRY_NAMES[ $id ];
884                 return $long;
885         } else {
886                 return false;
887         }
888 }
889
890 /**
891  * Return flag URL from 2 letter country code
892  *
893  */
894 function yourls_geo_get_flag( $code ) {
895         if( file_exists( YOURLS_INC.'/geo/flags/flag_'.strtolower($code).'.gif' ) ) {
896                 $img = yourls_match_current_protocol( YOURLS_SITE.'/includes/geo/flags/flag_'.( strtolower( $code ) ).'.gif' );
897         } else {
898                 $img = false;
899         }
900         return yourls_apply_filter( 'geo_get_flag', $img, $code );
901 }
902
903
904 /**
905  * Check if an upgrade is needed
906  *
907  */
908 function yourls_upgrade_is_needed() {
909         // check YOURLS_DB_VERSION exist && match values stored in YOURLS_DB_TABLE_OPTIONS
910         list( $currentver, $currentsql ) = yourls_get_current_version_from_sql();
911         if( $currentsql < YOURLS_DB_VERSION )
912                 return true;
913                 
914         return false;
915 }
916
917 /**
918  * Get current version & db version as stored in the options DB. Prior to 1.4 there's no option table.
919  *
920  */
921 function yourls_get_current_version_from_sql() {
922         $currentver = yourls_get_option( 'version' );
923         $currentsql = yourls_get_option( 'db_version' );
924
925         // Values if version is 1.3
926         if( !$currentver )
927                 $currentver = '1.3';
928         if( !$currentsql )
929                 $currentsql = '100';
930                 
931         return array( $currentver, $currentsql);
932 }
933
934 /**
935  * Read an option from DB (or from cache if available). Return value or $default if not found
936  *
937  */
938 function yourls_get_option( $option_name, $default = false ) {
939         global $ydb;
940         
941         // Allow plugins to short-circuit options
942         $pre = yourls_apply_filter( 'shunt_option_'.$option_name, false );
943         if ( false !== $pre )
944                 return $pre;
945
946         // If option not cached already, get its value from the DB
947         if ( !isset( $ydb->option[$option_name] ) ) {
948                 $table = YOURLS_DB_TABLE_OPTIONS;
949                 $option_name = yourls_escape( $option_name );
950                 $row = $ydb->get_row( "SELECT `option_value` FROM `$table` WHERE `option_name` = '$option_name' LIMIT 1" );
951                 if ( is_object( $row) ) { // Has to be get_row instead of get_var because of funkiness with 0, false, null values
952                         $value = $row->option_value;
953                 } else { // option does not exist, so we must cache its non-existence
954                         $value = $default;
955                 }
956                 $ydb->option[ $option_name ] = yourls_maybe_unserialize( $value );
957         }
958
959         return yourls_apply_filter( 'get_option_'.$option_name, $ydb->option[$option_name] );
960 }
961
962 /**
963  * Read all options from DB at once
964  *
965  */
966 function yourls_get_all_options() {
967         global $ydb;
968
969         // Allow plugins to short-circuit all options. (Note: regular plugins are loaded after all options)
970         $pre = yourls_apply_filter( 'shunt_all_options', false );
971         if ( false !== $pre )
972                 return $pre;
973
974         $table = YOURLS_DB_TABLE_OPTIONS;
975         
976         $allopt = $ydb->get_results( "SELECT `option_name`, `option_value` FROM `$table` WHERE 1=1" );
977         
978         foreach( (array)$allopt as $option ) {
979                 $ydb->option[$option->option_name] = yourls_maybe_unserialize( $option->option_value );
980         }
981         
982         $ydb->option = yourls_apply_filter( 'get_all_options', $ydb->option );
983 }
984
985 /**
986  * Update (add if doesn't exist) an option to DB
987  *
988  */
989 function yourls_update_option( $option_name, $newvalue ) {
990         global $ydb;
991         $table = YOURLS_DB_TABLE_OPTIONS;
992
993         $safe_option_name = yourls_escape( $option_name );
994
995         $oldvalue = yourls_get_option( $safe_option_name );
996
997         // If the new and old values are the same, no need to update.
998         if ( $newvalue === $oldvalue )
999                 return false;
1000
1001         if ( false === $oldvalue ) {
1002                 yourls_add_option( $option_name, $newvalue );
1003                 return true;
1004         }
1005
1006         $_newvalue = yourls_escape( yourls_maybe_serialize( $newvalue ) );
1007         
1008         yourls_do_action( 'update_option', $option_name, $oldvalue, $newvalue );
1009
1010         $ydb->query( "UPDATE `$table` SET `option_value` = '$_newvalue' WHERE `option_name` = '$option_name'" );
1011
1012         if ( $ydb->rows_affected == 1 ) {
1013                 $ydb->option[ $option_name ] = $newvalue;
1014                 return true;
1015         }
1016         return false;
1017 }
1018
1019 /**
1020  * Add an option to the DB
1021  *
1022  */
1023 function yourls_add_option( $name, $value = '' ) {
1024         global $ydb;
1025         $table = YOURLS_DB_TABLE_OPTIONS;
1026         $safe_name = yourls_escape( $name );
1027
1028         // Make sure the option doesn't already exist
1029         if ( false !== yourls_get_option( $safe_name ) )
1030                 return;
1031
1032         $_value = yourls_escape( yourls_maybe_serialize( $value ) );
1033
1034         yourls_do_action( 'add_option', $safe_name, $_value );
1035
1036         $ydb->query( "INSERT INTO `$table` (`option_name`, `option_value`) VALUES ('$name', '$_value')" );
1037         $ydb->option[ $name ] = $value;
1038         return;
1039 }
1040
1041
1042 /**
1043  * Delete an option from the DB
1044  *
1045  */
1046 function yourls_delete_option( $name ) {
1047         global $ydb;
1048         $table = YOURLS_DB_TABLE_OPTIONS;
1049         $name = yourls_escape( $name );
1050
1051         // Get the ID, if no ID then return
1052         $option = $ydb->get_row( "SELECT option_id FROM `$table` WHERE `option_name` = '$name'" );
1053         if ( is_null( $option ) || !$option->option_id )
1054                 return false;
1055                 
1056         yourls_do_action( 'delete_option', $option_name );
1057                 
1058         $ydb->query( "DELETE FROM `$table` WHERE `option_name` = '$name'" );
1059         return true;
1060 }
1061
1062
1063
1064 /**
1065  * Serialize data if needed. Stolen from WordPress
1066  *
1067  */
1068 function yourls_maybe_serialize( $data ) {
1069         if ( is_array( $data ) || is_object( $data ) )
1070                 return serialize( $data );
1071
1072         if ( yourls_is_serialized( $data ) )
1073                 return serialize( $data );
1074
1075         return $data;
1076 }
1077
1078 /**
1079  * Check value to find if it was serialized. Stolen from WordPress
1080  *
1081  */
1082 function yourls_is_serialized( $data ) {
1083         // if it isn't a string, it isn't serialized
1084         if ( !is_string( $data ) )
1085                 return false;
1086         $data = trim( $data );
1087         if ( 'N;' == $data )
1088                 return true;
1089         if ( !preg_match( '/^([adObis]):/', $data, $badions ) )
1090                 return false;
1091         switch ( $badions[1] ) {
1092                 case 'a' :
1093                 case 'O' :
1094                 case 's' :
1095                         if ( preg_match( "/^{$badions[1]}:[0-9]+:.*[;}]\$/s", $data ) )
1096                                 return true;
1097                         break;
1098                 case 'b' :
1099                 case 'i' :
1100                 case 'd' :
1101                         if ( preg_match( "/^{$badions[1]}:[0-9.E-]+;\$/", $data ) )
1102                                 return true;
1103                         break;
1104         }
1105         return false;
1106 }
1107
1108 /**
1109  * Unserialize value only if it was serialized. Stolen from WP
1110  *
1111  */
1112 function yourls_maybe_unserialize( $original ) {
1113         if ( yourls_is_serialized( $original ) ) // don't attempt to unserialize data that wasn't serialized going in
1114                 return @unserialize( $original );
1115         return $original;
1116 }
1117
1118 /**
1119  * Determine if the current page is private
1120  *
1121  */
1122 function yourls_is_private() {
1123         $private = false;
1124
1125         if ( defined('YOURLS_PRIVATE') && YOURLS_PRIVATE == true ) {
1126
1127                 // Allow overruling for particular pages:
1128                 
1129                 // API
1130                 if( yourls_is_API() ) {
1131                         if( !defined('YOURLS_PRIVATE_API') || YOURLS_PRIVATE_API != false )
1132                                 $private = true;                
1133
1134                 // Infos
1135                 } elseif( yourls_is_infos() ) {
1136                         if( !defined('YOURLS_PRIVATE_INFOS') || YOURLS_PRIVATE_INFOS !== false )
1137                                 $private = true;
1138                 
1139                 // Others
1140                 } else {
1141                         $private = true;
1142                 }
1143                 
1144         }
1145                         
1146         return yourls_apply_filter( 'is_private', $private );
1147 }
1148
1149 /**
1150  * Show login form if required
1151  *
1152  */
1153 function yourls_maybe_require_auth() {
1154         if( yourls_is_private() )
1155                 require_once( YOURLS_INC.'/auth.php' );
1156 }
1157
1158 /**
1159  * Allow several short URLs for the same long URL ?
1160  *
1161  */
1162 function yourls_allow_duplicate_longurls() {
1163         // special treatment if API to check for WordPress plugin requests
1164         if( yourls_is_API() ) {
1165                 if ( isset($_REQUEST['source']) && $_REQUEST['source'] == 'plugin' ) 
1166                         return false;
1167         }
1168         return ( defined( 'YOURLS_UNIQUE_URLS' ) && YOURLS_UNIQUE_URLS == false );
1169 }
1170
1171 /**
1172  * Return list of all shorturls associated to the same long URL. Returns NULL or array of keywords.
1173  *
1174  */
1175 function yourls_get_duplicate_keywords( $longurl ) {
1176         if( !yourls_allow_duplicate_longurls() )
1177                 return NULL;
1178         
1179         global $ydb;
1180         $longurl = yourls_escape( yourls_sanitize_url($longurl) );
1181         $table = YOURLS_DB_TABLE_URL;
1182         
1183         $return = $ydb->get_col( "SELECT `keyword` FROM `$table` WHERE `url` = '$longurl'" );
1184         return yourls_apply_filter( 'get_duplicate_keywords', $return, $longurl );
1185 }
1186
1187 /**
1188  * Check if an IP shortens URL too fast to prevent DB flood. Return true, or die.
1189  *
1190  */
1191 function yourls_check_IP_flood( $ip = '' ) {
1192
1193         // Allow plugins to short-circuit the whole function
1194         $pre = yourls_apply_filter( 'shunt_check_IP_flood', false, $ip );
1195         if ( false !== $pre )
1196                 return $pre;
1197
1198         yourls_do_action( 'pre_check_ip_flood', $ip ); // at this point $ip can be '', check it if your plugin hooks in here
1199
1200         if(
1201                 ( defined('YOURLS_FLOOD_DELAY_SECONDS') && YOURLS_FLOOD_DELAY_SECONDS === 0 ) ||
1202                 !defined('YOURLS_FLOOD_DELAY_SECONDS')
1203         )
1204                 return true;
1205
1206         $ip = ( $ip ? yourls_sanitize_ip( $ip ) : yourls_get_IP() );
1207
1208         // Don't throttle whitelist IPs
1209         if( defined( 'YOURLS_FLOOD_IP_WHITELIST' ) && YOURLS_FLOOD_IP_WHITELIST ) {
1210                 $whitelist_ips = explode( ',', YOURLS_FLOOD_IP_WHITELIST );
1211                 foreach( (array)$whitelist_ips as $whitelist_ip ) {
1212                         $whitelist_ip = trim( $whitelist_ip );
1213                         if ( $whitelist_ip == $ip )
1214                                 return true;
1215                 }
1216         }
1217         
1218         // Don't throttle logged in users
1219         if( yourls_is_private() ) {
1220                  if( yourls_is_valid_user() === true )
1221                         return true;
1222         }
1223         
1224         yourls_do_action( 'check_ip_flood', $ip );
1225         
1226         global $ydb;
1227         $table = YOURLS_DB_TABLE_URL;
1228         
1229         $lasttime = $ydb->get_var( "SELECT `timestamp` FROM $table WHERE `ip` = '$ip' ORDER BY `timestamp` DESC LIMIT 1" );
1230         if( $lasttime ) {
1231                 $now = date( 'U' );
1232                 $then = date( 'U', strtotime( $lasttime ) );
1233                 if( ( $now - $then ) <= YOURLS_FLOOD_DELAY_SECONDS ) {
1234                         // Flood!
1235                         yourls_do_action( 'ip_flood', $ip, $now - $then );
1236                         yourls_die( yourls__( 'Too many URLs added too fast. Slow down please.' ), yourls__( 'Forbidden' ), 403 );
1237                 }
1238         }
1239         
1240         return true;
1241 }
1242
1243 /**
1244  * Check if YOURLS is installing
1245  *
1246  * @return bool
1247  * @since 1.6
1248  */
1249 function yourls_is_installing() {
1250         $installing = defined( 'YOURLS_INSTALLING' ) && YOURLS_INSTALLING == true;
1251         return yourls_apply_filter( 'is_installing', $installing );
1252 }
1253
1254 /**
1255  * Check if YOURLS is upgrading
1256  *
1257  * @return bool
1258  * @since 1.6
1259  */
1260 function yourls_is_upgrading() {
1261         $upgrading = defined( 'YOURLS_UPGRADING' ) && YOURLS_UPGRADING == true;
1262         return yourls_apply_filter( 'is_upgrading', $upgrading );
1263 }
1264
1265
1266 /**
1267  * Check if YOURLS is installed
1268  *
1269  */
1270 function yourls_is_installed() {
1271         static $is_installed = false;
1272         if ( $is_installed === false ) {
1273                 $check_14 = $check_13 = false;
1274                 global $ydb;
1275                 if( defined('YOURLS_DB_TABLE_NEXTDEC') )
1276                         $check_13 = $ydb->get_var('SELECT `next_id` FROM '.YOURLS_DB_TABLE_NEXTDEC);
1277                 $check_14 = yourls_get_option( 'version' );
1278                 $is_installed = $check_13 || $check_14;
1279         }
1280         return yourls_apply_filter( 'is_installed', $is_installed );
1281 }
1282
1283 /**
1284  * Generate random string of (int)$length length and type $type (see function for details)
1285  *
1286  */
1287 function yourls_rnd_string ( $length = 5, $type = 0, $charlist = '' ) {
1288         $str = '';
1289         $length = intval( $length );
1290
1291         // define possible characters
1292         switch ( $type ) {
1293
1294                 // custom char list, or comply to charset as defined in config
1295                 case '0':
1296                         $possible = $charlist ? $charlist : yourls_get_shorturl_charset() ;
1297                         break;
1298         
1299                 // no vowels to make no offending word, no 0/1/o/l to avoid confusion between letters & digits. Perfect for passwords.
1300                 case '1':
1301                         $possible = "23456789bcdfghjkmnpqrstvwxyz";
1302                         break;
1303                 
1304                 // Same, with lower + upper
1305                 case '2':
1306                         $possible = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ";
1307                         break;
1308                 
1309                 // all letters, lowercase
1310                 case '3':
1311                         $possible = "abcdefghijklmnopqrstuvwxyz";
1312                         break;
1313                 
1314                 // all letters, lowercase + uppercase
1315                 case '4':
1316                         $possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1317                         break;
1318                 
1319                 // all digits & letters lowercase 
1320                 case '5':
1321                         $possible = "0123456789abcdefghijklmnopqrstuvwxyz";
1322                         break;
1323                 
1324                 // all digits & letters lowercase + uppercase
1325                 case '6':
1326                         $possible = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1327                         break;
1328                 
1329         }
1330
1331         $i = 0;
1332         while ($i < $length) {
1333                 $str .= substr($possible, mt_rand(0, strlen($possible)-1), 1);
1334                 $i++;
1335         }
1336         
1337         return yourls_apply_filter( 'rnd_string', $str, $length, $type, $charlist );
1338 }
1339
1340 /**
1341  * Return salted string
1342  *
1343  */
1344 function yourls_salt( $string ) {
1345         $salt = defined('YOURLS_COOKIEKEY') ? YOURLS_COOKIEKEY : md5(__FILE__) ;
1346         return yourls_apply_filter( 'yourls_salt', md5 ($string . $salt), $string );
1347 }
1348
1349 /**
1350  * Add a query var to a URL and return URL. Completely stolen from WP.
1351  * 
1352  * Works with one of these parameter patterns:
1353  *     array( 'var' => 'value' )
1354  *     array( 'var' => 'value' ), $url
1355  *     'var', 'value'
1356  *     'var', 'value', $url 
1357  * If $url ommited, uses $_SERVER['REQUEST_URI']
1358  *
1359  */
1360 function yourls_add_query_arg() {
1361         $ret = '';
1362         if ( is_array( func_get_arg(0) ) ) {
1363                 if ( @func_num_args() < 2 || false === @func_get_arg( 1 ) )
1364                         $uri = $_SERVER['REQUEST_URI'];
1365                 else
1366                         $uri = @func_get_arg( 1 );
1367         } else {
1368                 if ( @func_num_args() < 3 || false === @func_get_arg( 2 ) )
1369                         $uri = $_SERVER['REQUEST_URI'];
1370                 else
1371                         $uri = @func_get_arg( 2 );
1372         }
1373         
1374         $uri = str_replace( '&amp;', '&', $uri );
1375
1376         
1377         if ( $frag = strstr( $uri, '#' ) )
1378                 $uri = substr( $uri, 0, -strlen( $frag ) );
1379         else
1380                 $frag = '';
1381
1382         if ( preg_match( '|^https?://|i', $uri, $matches ) ) {
1383                 $protocol = $matches[0];
1384                 $uri = substr( $uri, strlen( $protocol ) );
1385         } else {
1386                 $protocol = '';
1387         }
1388
1389         if ( strpos( $uri, '?' ) !== false ) {
1390                 $parts = explode( '?', $uri, 2 );
1391                 if ( 1 == count( $parts ) ) {
1392                         $base = '?';
1393                         $query = $parts[0];
1394                 } else {
1395                         $base = $parts[0] . '?';
1396                         $query = $parts[1];
1397                 }
1398         } elseif ( !empty( $protocol ) || strpos( $uri, '=' ) === false ) {
1399                 $base = $uri . '?';
1400                 $query = '';
1401         } else {
1402                 $base = '';
1403                 $query = $uri;
1404         }
1405
1406         parse_str( $query, $qs );
1407         $qs = yourls_urlencode_deep( $qs ); // this re-URL-encodes things that were already in the query string
1408         if ( is_array( func_get_arg( 0 ) ) ) {
1409                 $kayvees = func_get_arg( 0 );
1410                 $qs = array_merge( $qs, $kayvees );
1411         } else {
1412                 $qs[func_get_arg( 0 )] = func_get_arg( 1 );
1413         }
1414
1415         foreach ( (array) $qs as $k => $v ) {
1416                 if ( $v === false )
1417                         unset( $qs[$k] );
1418         }
1419
1420         $ret = http_build_query( $qs );
1421         $ret = trim( $ret, '?' );
1422         $ret = preg_replace( '#=(&|$)#', '$1', $ret );
1423         $ret = $protocol . $base . $ret . $frag;
1424         $ret = rtrim( $ret, '?' );
1425         return $ret;
1426 }
1427
1428 /**
1429  * Navigates through an array and encodes the values to be used in a URL. Stolen from WP, used in yourls_add_query_arg()
1430  *
1431  */
1432 function yourls_urlencode_deep( $value ) {
1433         $value = is_array( $value ) ? array_map( 'yourls_urlencode_deep', $value ) : urlencode( $value );
1434         return $value;
1435 }
1436
1437 /**
1438  * Remove arg from query. Opposite of yourls_add_query_arg. Stolen from WP.
1439  *
1440  */
1441 function yourls_remove_query_arg( $key, $query = false ) {
1442         if ( is_array( $key ) ) { // removing multiple keys
1443                 foreach ( $key as $k )
1444                         $query = yourls_add_query_arg( $k, false, $query );
1445                 return $query;
1446         }
1447         return yourls_add_query_arg( $key, false, $query );
1448 }
1449
1450 /**
1451  * Return a time-dependent string for nonce creation
1452  *
1453  */
1454 function yourls_tick() {
1455         return ceil( time() / YOURLS_NONCE_LIFE );
1456 }
1457
1458 /**
1459  * Create a time limited, action limited and user limited token
1460  *
1461  */
1462 function yourls_create_nonce( $action, $user = false ) {
1463         if( false == $user )
1464                 $user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
1465         $tick = yourls_tick();
1466         return substr( yourls_salt($tick . $action . $user), 0, 10 );
1467 }
1468
1469 /**
1470  * Create a nonce field for inclusion into a form
1471  *
1472  */
1473 function yourls_nonce_field( $action, $name = 'nonce', $user = false, $echo = true ) {
1474         $field = '<input type="hidden" id="'.$name.'" name="'.$name.'" value="'.yourls_create_nonce( $action, $user ).'" />';
1475         if( $echo )
1476                 echo $field."\n";
1477         return $field;
1478 }
1479
1480 /**
1481  * Add a nonce to a URL. If URL omitted, adds nonce to current URL
1482  *
1483  */
1484 function yourls_nonce_url( $action, $url = false, $name = 'nonce', $user = false ) {
1485         $nonce = yourls_create_nonce( $action, $user );
1486         return yourls_add_query_arg( $name, $nonce, $url );
1487 }
1488
1489 /**
1490  * Check validity of a nonce (ie time span, user and action match).
1491  * 
1492  * Returns true if valid, dies otherwise (yourls_die() or die($return) if defined)
1493  * if $nonce is false or unspecified, it will use $_REQUEST['nonce']
1494  *
1495  */
1496 function yourls_verify_nonce( $action, $nonce = false, $user = false, $return = '' ) {
1497         // get user
1498         if( false == $user )
1499                 $user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
1500                 
1501         // get current nonce value
1502         if( false == $nonce && isset( $_REQUEST['nonce'] ) )
1503                 $nonce = $_REQUEST['nonce'];
1504
1505         // what nonce should be
1506         $valid = yourls_create_nonce( $action, $user );
1507         
1508         if( $nonce == $valid ) {
1509                 return true;
1510         } else {
1511                 if( $return )
1512                         die( $return );
1513                 yourls_die( yourls__( 'Unauthorized action or expired link' ), yourls__( 'Error' ), 403 );
1514         }
1515 }
1516
1517 /**
1518  * Converts keyword into short link (prepend with YOURLS base URL)
1519  *
1520  */
1521 function yourls_link( $keyword = '' ) {
1522         $link = YOURLS_SITE . '/' . yourls_sanitize_keyword( $keyword );
1523         return yourls_apply_filter( 'yourls_link', $link, $keyword );
1524 }
1525
1526 /**
1527  * Converts keyword into stat link (prepend with YOURLS base URL, append +)
1528  *
1529  */
1530 function yourls_statlink( $keyword = '' ) {
1531         $link = YOURLS_SITE . '/' . yourls_sanitize_keyword( $keyword ) . '+';
1532         if( yourls_is_ssl() )
1533                 $link = str_replace( 'http://', 'https://', $link );
1534         return yourls_apply_filter( 'yourls_statlink', $link, $keyword );
1535 }
1536
1537 /**
1538  * Check if we're in API mode. Returns bool
1539  *
1540  */
1541 function yourls_is_API() {
1542         if ( defined( 'YOURLS_API' ) && YOURLS_API == true )
1543                 return true;
1544         return false;
1545 }
1546
1547 /**
1548  * Check if we're in Ajax mode. Returns bool
1549  *
1550  */
1551 function yourls_is_Ajax() {
1552         if ( defined( 'YOURLS_AJAX' ) && YOURLS_AJAX == true )
1553                 return true;
1554         return false;
1555 }
1556
1557 /**
1558  * Check if we're in GO mode (yourls-go.php). Returns bool
1559  *
1560  */
1561 function yourls_is_GO() {
1562         if ( defined( 'YOURLS_GO' ) && YOURLS_GO == true )
1563                 return true;
1564         return false;
1565 }
1566
1567 /**
1568  * Check if we're displaying stats infos (yourls-infos.php). Returns bool
1569  *
1570  */
1571 function yourls_is_infos() {
1572         if ( defined( 'YOURLS_INFOS' ) && YOURLS_INFOS == true )
1573                 return true;
1574         return false;
1575 }
1576
1577 /**
1578  * Check if we'll need interface display function (ie not API or redirection)
1579  *
1580  */
1581 function yourls_has_interface() {
1582         if( yourls_is_API() or yourls_is_GO() )
1583                 return false;
1584         return true;
1585 }
1586
1587 /**
1588  * Check if we're in the admin area. Returns bool
1589  *
1590  */
1591 function yourls_is_admin() {
1592         if ( defined( 'YOURLS_ADMIN' ) && YOURLS_ADMIN == true )
1593                 return true;
1594         return false;
1595 }
1596
1597 /**
1598  * Check if the server seems to be running on Windows. Not exactly sure how reliable this is.
1599  *
1600  */
1601 function yourls_is_windows() {
1602         return defined( 'DIRECTORY_SEPARATOR' ) && DIRECTORY_SEPARATOR == '\\';
1603 }
1604
1605 /**
1606  * Check if SSL is required. Returns bool.
1607  *
1608  */
1609 function yourls_needs_ssl() {
1610         if ( defined('YOURLS_ADMIN_SSL') && YOURLS_ADMIN_SSL == true )
1611                 return true;
1612         return false;
1613 }
1614
1615 /**
1616  * Return admin link, with SSL preference if applicable.
1617  *
1618  */
1619 function yourls_admin_url( $page = '' ) {
1620         $admin = YOURLS_SITE . '/admin/' . $page;
1621         if( yourls_is_ssl() or yourls_needs_ssl() )
1622                 $admin = str_replace('http://', 'https://', $admin);
1623         return yourls_apply_filter( 'admin_url', $admin, $page );
1624 }
1625
1626 /**
1627  * Return YOURLS_SITE or URL under YOURLS setup, with SSL preference
1628  *
1629  */
1630 function yourls_site_url( $echo = true, $url = '' ) {
1631         $url = yourls_get_relative_url( $url );
1632         $url = trim( YOURLS_SITE . '/' . $url, '/' );
1633         
1634         // Do not enforce (checking yourls_need_ssl() ) but check current usage so it won't force SSL on non-admin pages
1635         if( yourls_is_ssl() )
1636                 $url = str_replace( 'http://', 'https://', $url );
1637         $url = yourls_apply_filter( 'site_url', $url );
1638         if( $echo )
1639                 echo $url;
1640         return $url;
1641 }
1642
1643 /**
1644  * Check if SSL is used, returns bool. Stolen from WP.
1645  *
1646  */
1647 function yourls_is_ssl() {
1648         $is_ssl = false;
1649         if ( isset( $_SERVER['HTTPS'] ) ) {
1650                 if ( 'on' == strtolower( $_SERVER['HTTPS'] ) )
1651                         $is_ssl = true;
1652                 if ( '1' == $_SERVER['HTTPS'] )
1653                         $is_ssl = true;
1654         } elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
1655                 $is_ssl = true;
1656         }
1657         return yourls_apply_filter( 'is_ssl', $is_ssl );
1658 }
1659
1660
1661 /**
1662  * Get a remote page <title>, return a string (either title or url)
1663  *
1664  */
1665 function yourls_get_remote_title( $url ) {
1666         // Allow plugins to short-circuit the whole function
1667         $pre = yourls_apply_filter( 'shunt_get_remote_title', false, $url );
1668         if ( false !== $pre )
1669                 return $pre;
1670
1671         require_once( YOURLS_INC.'/functions-http.php' );
1672
1673         $url = yourls_sanitize_url( $url );
1674
1675         $title = $charset = false;
1676         
1677         $content = yourls_get_remote_content( $url );
1678         
1679         // If false, return url as title.
1680         // Todo: improve this with temporary title when shorturl_meta available?
1681         if( false === $content )
1682                 return $url;
1683
1684         if( $content !== false ) {
1685                 // look for <title>
1686                 if ( preg_match('/<title>(.*?)<\/title>/is', $content, $found ) ) {
1687                         $title = $found[1];
1688                         unset( $found );
1689                 }
1690
1691                 // look for charset
1692                 // <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
1693                 if ( preg_match('/<meta[^>]*?charset=([^>]*?)\/?>/is', $content, $found ) ) {
1694                         $charset = trim($found[1], '"\' ');
1695                         unset( $found );
1696                 }
1697         }
1698         
1699         // if title not found, guess if returned content was actually an error message
1700         if( $title == false && strpos( $content, 'Error' ) === 0 ) {
1701                 $title = $content;
1702         }
1703         
1704         if( $title == false )
1705                 $title = $url;
1706         
1707         /*
1708         if( !yourls_seems_utf8( $title ) )
1709                 $title = utf8_encode( $title );
1710         */
1711         
1712         // Charset conversion. We use @ to remove warnings (mb_ functions are easily bitching about illegal chars)
1713         if( function_exists( 'mb_convert_encoding' ) ) {
1714                 if( $charset ) {
1715                         $title = @mb_convert_encoding( $title, 'UTF-8', $charset );
1716                 } else {
1717                         $title = @mb_convert_encoding( $title, 'UTF-8' );
1718                 }
1719         }
1720         
1721         // Remove HTML entities
1722         $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' );
1723         
1724         // Strip out evil things
1725         $title = yourls_sanitize_title( $title );
1726         
1727         return yourls_apply_filter( 'get_remote_title', $title, $url );
1728 }
1729
1730 /**
1731  * Quick UA check for mobile devices. Return boolean.
1732  *
1733  */
1734 function yourls_is_mobile_device() {
1735         // Strings searched
1736         $mobiles = array(
1737                 'android', 'blackberry', 'blazer',
1738                 'compal', 'elaine', 'fennec', 'hiptop',
1739                 'iemobile', 'iphone', 'ipod', 'ipad',
1740                 'iris', 'kindle', 'opera mobi', 'opera mini',
1741                 'palm', 'phone', 'pocket', 'psp', 'symbian',
1742                 'treo', 'wap', 'windows ce', 'windows phone'
1743         );
1744         
1745         // Current user-agent
1746         $current = strtolower( $_SERVER['HTTP_USER_AGENT'] );
1747         
1748         // Check and return
1749         $is_mobile = ( str_replace( $mobiles, '', $current ) != $current );
1750         return yourls_apply_filter( 'is_mobile_device', $is_mobile );
1751 }
1752
1753 /**
1754  * Get request in YOURLS base (eg in 'http://site.com/yourls/abcd' get 'abdc')
1755  *
1756  */
1757 function yourls_get_request() {
1758         // Allow plugins to short-circuit the whole function
1759         $pre = yourls_apply_filter( 'shunt_get_request', false );
1760         if ( false !== $pre )
1761                 return $pre;
1762                 
1763         static $request = null;
1764
1765         yourls_do_action( 'pre_get_request', $request );
1766         
1767         if( $request !== null )
1768                 return $request;
1769         
1770         // Ignore protocol & www. prefix
1771         $root = str_replace( array( 'https://', 'http://', 'https://www.', 'http://www.' ), '', YOURLS_SITE );
1772         // Case insensitive comparison of the YOURLS root to match both http://Sho.rt/blah and http://sho.rt/blah
1773         $request = preg_replace( "!$root/!i", '', $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 1 );
1774
1775         // Unless request looks like a full URL (ie request is a simple keyword) strip query string
1776         if( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) {
1777                 $request = current( explode( '?', $request ) );
1778         }
1779         
1780         return yourls_apply_filter( 'get_request', $request );
1781 }
1782
1783 /**
1784  * Change protocol to match current scheme used (http or https)
1785  *
1786  */
1787 function yourls_match_current_protocol( $url, $normal = 'http', $ssl = 'https' ) {
1788         if( yourls_is_ssl() )
1789                 $url = str_replace( $normal, $ssl, $url );
1790         return yourls_apply_filter( 'match_current_protocol', $url );
1791 }
1792
1793 /**
1794  * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP.
1795  *
1796  */
1797 function yourls_fix_request_uri() {
1798
1799         $default_server_values = array(
1800                 'SERVER_SOFTWARE' => '',
1801                 'REQUEST_URI' => '',
1802         );
1803         $_SERVER = array_merge( $default_server_values, $_SERVER );
1804
1805         // Fix for IIS when running with PHP ISAPI
1806         if ( empty( $_SERVER['REQUEST_URI'] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER['SERVER_SOFTWARE'] ) ) ) {
1807
1808                 // IIS Mod-Rewrite
1809                 if ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] ) ) {
1810                         $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
1811                 }
1812                 // IIS Isapi_Rewrite
1813                 else if ( isset( $_SERVER['HTTP_X_REWRITE_URL'] ) ) {
1814                         $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_REWRITE_URL'];
1815                 } else {
1816                         // Use ORIG_PATH_INFO if there is no PATH_INFO
1817                         if ( !isset( $_SERVER['PATH_INFO'] ) && isset( $_SERVER['ORIG_PATH_INFO'] ) )
1818                                 $_SERVER['PATH_INFO'] = $_SERVER['ORIG_PATH_INFO'];
1819
1820                         // Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice)
1821                         if ( isset( $_SERVER['PATH_INFO'] ) ) {
1822                                 if ( $_SERVER['PATH_INFO'] == $_SERVER['SCRIPT_NAME'] )
1823                                         $_SERVER['REQUEST_URI'] = $_SERVER['PATH_INFO'];
1824                                 else
1825                                         $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] . $_SERVER['PATH_INFO'];
1826                         }
1827
1828                         // Append the query string if it exists and isn't null
1829                         if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
1830                                 $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
1831                         }
1832                 }
1833         }
1834 }
1835
1836 /**
1837  * Shutdown function, runs just before PHP shuts down execution. Stolen from WP
1838  *
1839  */
1840 function yourls_shutdown() {
1841         yourls_do_action( 'shutdown' );
1842 }
1843
1844 /**
1845  * Auto detect custom favicon in /user directory, fallback to YOURLS favicon, and echo/return its URL
1846  *
1847  */
1848 function yourls_favicon( $echo = true ) {
1849         static $favicon = null;
1850         if( $favicon !== null )
1851                 return $favicon;
1852         
1853         $custom = null;
1854         // search for favicon.(gif|ico|png|jpg|svg)
1855         foreach( array( 'gif', 'ico', 'png', 'jpg', 'svg' ) as $ext ) {
1856                 if( file_exists( YOURLS_USERDIR. '/favicon.' . $ext ) ) {
1857                         $custom = 'favicon.' . $ext;
1858                         break;
1859                 }
1860         }
1861         
1862         if( $custom ) {
1863                 $favicon = yourls_site_url( false, YOURLS_USERURL . '/' . $custom );
1864         } else {
1865                 $favicon = yourls_site_url( false ) . '/images/favicon.gif';
1866         }
1867         if( $echo )
1868                 echo $favicon;
1869         return $favicon;
1870 }
1871
1872 /**
1873  * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP.
1874  *
1875  */
1876 function yourls_check_maintenance_mode() {
1877
1878         $file = YOURLS_ABSPATH . '/.maintenance' ;
1879         if ( !file_exists( $file ) || yourls_is_upgrading() || yourls_is_installing() )
1880                 return;
1881         
1882         global $maintenance_start;
1883
1884         include( $file );
1885         // If the $maintenance_start timestamp is older than 10 minutes, don't die.
1886         if ( ( time() - $maintenance_start ) >= 600 )
1887                 return;
1888
1889         // Use any /user/maintenance.php file
1890         if( file_exists( YOURLS_USERDIR.'/maintenance.php' ) ) {
1891                 include( YOURLS_USERDIR.'/maintenance.php' );
1892                 die();
1893         }
1894         
1895         // https://www.youtube.com/watch?v=Xw-m4jEY-Ns
1896         $title   = yourls__( 'Service temporarily unavailable' );
1897         $message = yourls__( 'Our service is currently undergoing scheduled maintenance.' ) . "</p>\n<p>" .
1898         yourls__( 'Things should not last very long, thank you for your patience and please excuse the inconvenience' );
1899         yourls_die( $message, $title , 503 );
1900
1901 }
1902
1903 /**
1904  * Return current admin page, or null if not an admin page
1905  *
1906  * @return mixed string if admin page, null if not an admin page
1907  * @since 1.6
1908  */
1909 function yourls_current_admin_page() {
1910         if( yourls_is_admin() ) {
1911                 $current = substr( yourls_get_request(), 6 );
1912                 if( $current === false ) 
1913                         $current = 'index.php'; // if current page is http://sho.rt/admin/ instead of http://sho.rt/admin/index.php
1914                         
1915                 return $current;
1916         }
1917         return null;
1918 }
1919
1920 /**
1921  * Check if a URL protocol is allowed
1922  *
1923  * Checks a URL against a list of whitelisted protocols. Protocols must be defined with
1924  * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid
1925  * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either
1926  *
1927  * @since 1.6
1928  *
1929  * @param string $url URL to be check
1930  * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols
1931  * @return boolean true if protocol allowed, false otherwise
1932  */
1933 function yourls_is_allowed_protocol( $url, $protocols = array() ) {
1934         if( ! $protocols ) {
1935                 global $yourls_allowedprotocols;
1936                 $protocols = $yourls_allowedprotocols;
1937         }
1938         
1939         $protocol = yourls_get_protocol( $url );
1940         return yourls_apply_filter( 'is_allowed_protocol', in_array( $protocol, $protocols ), $url, $protocols );
1941 }
1942
1943 /**
1944  * Get protocol from a URL (eg mailto:, http:// ...)
1945  *
1946  * @since 1.6
1947  *
1948  * @param string $url URL to be check
1949  * @return string Protocol, with slash slash if applicable. Empty string if no protocol
1950  */
1951 function yourls_get_protocol( $url ) {
1952         preg_match( '!^[a-zA-Z0-9\+\.-]+:(//)?!', $url, $matches );
1953         /*
1954         http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
1955         The scheme name consists of a sequence of characters beginning with a letter and followed by any
1956         combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are
1957         case-insensitive, the canonical form is lowercase and documents that specify schemes must do so
1958         with lowercase letters. It is followed by a colon (":").
1959         */
1960         $protocol = ( isset( $matches[0] ) ? $matches[0] : '' );
1961         return yourls_apply_filter( 'get_protocol', $protocol, $url );
1962 }
1963
1964 /**
1965  * Get relative URL (eg 'abc' from 'http://sho.rt/abc')
1966  *
1967  * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is
1968  * or return empty string if $strict is true
1969  *
1970  * @since 1.6
1971  * @param string $url URL to relativize
1972  * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string
1973  * @return string URL 
1974  */
1975 function yourls_get_relative_url( $url, $strict = true ) {
1976         $url = yourls_sanitize_url( $url );
1977
1978         // Remove protocols to make it easier
1979         $noproto_url  = str_replace( 'https:', 'http:', $url );
1980         $noproto_site = str_replace( 'https:', 'http:', YOURLS_SITE );
1981         
1982         // Trim URL from YOURLS root URL : if no modification made, URL wasn't relative
1983         $_url = str_replace( $noproto_site . '/', '', $noproto_url );
1984         if( $_url == $noproto_url )
1985                 $_url = ( $strict ? '' : $url );
1986
1987         return yourls_apply_filter( 'get_relative_url', $_url, $url );
1988 }
1989
1990 /**
1991  * Marks a function as deprecated and informs when it has been used. Stolen from WP.
1992  *
1993  * There is a hook deprecated_function that will be called that can be used
1994  * to get the backtrace up to what file and function called the deprecated
1995  * function.
1996  *
1997  * The current behavior is to trigger a user error if YOURLS_DEBUG is true.
1998  *
1999  * This function is to be used in every function that is deprecated.
2000  *
2001  * @since 1.6
2002  * @uses yourls_do_action() Calls 'deprecated_function' and passes the function name, what to use instead,
2003  *   and the version the function was deprecated in.
2004  * @uses yourls_apply_filters() Calls 'deprecated_function_trigger_error' and expects boolean value of true to do
2005  *   trigger or false to not trigger error.
2006  *
2007  * @param string $function The function that was called
2008  * @param string $version The version of WordPress that deprecated the function
2009  * @param string $replacement Optional. The function that should have been called
2010  */
2011 function yourls_deprecated_function( $function, $version, $replacement = null ) {
2012
2013         yourls_do_action( 'deprecated_function', $function, $replacement, $version );
2014
2015         // Allow plugin to filter the output error trigger
2016         if ( YOURLS_DEBUG && yourls_apply_filters( 'deprecated_function_trigger_error', true ) ) {
2017                 if ( ! is_null( $replacement ) )
2018                         trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) );
2019                 else
2020                         trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.'), $function, $version ) );
2021         }
2022 }
2023
2024 /**
2025  * Return the value if not an empty string
2026  *
2027  * Used with array_filter(), to remove empty keys but not keys with value 0 or false
2028  *
2029  * @since 1.6
2030  * @param mixed $val Value to test against ''
2031  * @return bool True if not an empty string
2032  */
2033 function yourls_return_if_not_empty_string( $val ) {
2034         return( $val !== '' );
2035 }