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