]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/functions.php
Stuff related to public shortening[1] & user auth[2]
[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                 yourls_do_action( 'require_auth' );
1156                 require_once( YOURLS_INC.'/auth.php' );
1157         } else {
1158                 yourls_do_action( 'require_no_auth' );
1159         }
1160 }
1161
1162 /**
1163  * Allow several short URLs for the same long URL ?
1164  *
1165  */
1166 function yourls_allow_duplicate_longurls() {
1167         // special treatment if API to check for WordPress plugin requests
1168         if( yourls_is_API() ) {
1169                 if ( isset($_REQUEST['source']) && $_REQUEST['source'] == 'plugin' ) 
1170                         return false;
1171         }
1172         return ( defined( 'YOURLS_UNIQUE_URLS' ) && YOURLS_UNIQUE_URLS == false );
1173 }
1174
1175 /**
1176  * Return list of all shorturls associated to the same long URL. Returns NULL or array of keywords.
1177  *
1178  */
1179 function yourls_get_duplicate_keywords( $longurl ) {
1180         if( !yourls_allow_duplicate_longurls() )
1181                 return NULL;
1182         
1183         global $ydb;
1184         $longurl = yourls_escape( yourls_sanitize_url($longurl) );
1185         $table = YOURLS_DB_TABLE_URL;
1186         
1187         $return = $ydb->get_col( "SELECT `keyword` FROM `$table` WHERE `url` = '$longurl'" );
1188         return yourls_apply_filter( 'get_duplicate_keywords', $return, $longurl );
1189 }
1190
1191 /**
1192  * Check if an IP shortens URL too fast to prevent DB flood. Return true, or die.
1193  *
1194  */
1195 function yourls_check_IP_flood( $ip = '' ) {
1196
1197         // Allow plugins to short-circuit the whole function
1198         $pre = yourls_apply_filter( 'shunt_check_IP_flood', false, $ip );
1199         if ( false !== $pre )
1200                 return $pre;
1201
1202         yourls_do_action( 'pre_check_ip_flood', $ip ); // at this point $ip can be '', check it if your plugin hooks in here
1203
1204         if(
1205                 ( defined('YOURLS_FLOOD_DELAY_SECONDS') && YOURLS_FLOOD_DELAY_SECONDS === 0 ) ||
1206                 !defined('YOURLS_FLOOD_DELAY_SECONDS')
1207         )
1208                 return true;
1209
1210         $ip = ( $ip ? yourls_sanitize_ip( $ip ) : yourls_get_IP() );
1211
1212         // Don't throttle whitelist IPs
1213         if( defined( 'YOURLS_FLOOD_IP_WHITELIST' ) && YOURLS_FLOOD_IP_WHITELIST ) {
1214                 $whitelist_ips = explode( ',', YOURLS_FLOOD_IP_WHITELIST );
1215                 foreach( (array)$whitelist_ips as $whitelist_ip ) {
1216                         $whitelist_ip = trim( $whitelist_ip );
1217                         if ( $whitelist_ip == $ip )
1218                                 return true;
1219                 }
1220         }
1221         
1222         // Don't throttle logged in users
1223         if( yourls_is_private() ) {
1224                  if( yourls_is_valid_user() === true )
1225                         return true;
1226         }
1227         
1228         yourls_do_action( 'check_ip_flood', $ip );
1229         
1230         global $ydb;
1231         $table = YOURLS_DB_TABLE_URL;
1232         
1233         $lasttime = $ydb->get_var( "SELECT `timestamp` FROM $table WHERE `ip` = '$ip' ORDER BY `timestamp` DESC LIMIT 1" );
1234         if( $lasttime ) {
1235                 $now = date( 'U' );
1236                 $then = date( 'U', strtotime( $lasttime ) );
1237                 if( ( $now - $then ) <= YOURLS_FLOOD_DELAY_SECONDS ) {
1238                         // Flood!
1239                         yourls_do_action( 'ip_flood', $ip, $now - $then );
1240                         yourls_die( yourls__( 'Too many URLs added too fast. Slow down please.' ), yourls__( 'Forbidden' ), 403 );
1241                 }
1242         }
1243         
1244         return true;
1245 }
1246
1247 /**
1248  * Check if YOURLS is installing
1249  *
1250  * @return bool
1251  * @since 1.6
1252  */
1253 function yourls_is_installing() {
1254         $installing = defined( 'YOURLS_INSTALLING' ) && YOURLS_INSTALLING == true;
1255         return yourls_apply_filter( 'is_installing', $installing );
1256 }
1257
1258 /**
1259  * Check if YOURLS is upgrading
1260  *
1261  * @return bool
1262  * @since 1.6
1263  */
1264 function yourls_is_upgrading() {
1265         $upgrading = defined( 'YOURLS_UPGRADING' ) && YOURLS_UPGRADING == true;
1266         return yourls_apply_filter( 'is_upgrading', $upgrading );
1267 }
1268
1269
1270 /**
1271  * Check if YOURLS is installed
1272  *
1273  */
1274 function yourls_is_installed() {
1275         static $is_installed = false;
1276         if ( $is_installed === false ) {
1277                 $check_14 = $check_13 = false;
1278                 global $ydb;
1279                 if( defined('YOURLS_DB_TABLE_NEXTDEC') )
1280                         $check_13 = $ydb->get_var('SELECT `next_id` FROM '.YOURLS_DB_TABLE_NEXTDEC);
1281                 $check_14 = yourls_get_option( 'version' );
1282                 $is_installed = $check_13 || $check_14;
1283         }
1284         return yourls_apply_filter( 'is_installed', $is_installed );
1285 }
1286
1287 /**
1288  * Generate random string of (int)$length length and type $type (see function for details)
1289  *
1290  */
1291 function yourls_rnd_string ( $length = 5, $type = 0, $charlist = '' ) {
1292         $str = '';
1293         $length = intval( $length );
1294
1295         // define possible characters
1296         switch ( $type ) {
1297
1298                 // custom char list, or comply to charset as defined in config
1299                 case '0':
1300                         $possible = $charlist ? $charlist : yourls_get_shorturl_charset() ;
1301                         break;
1302         
1303                 // no vowels to make no offending word, no 0/1/o/l to avoid confusion between letters & digits. Perfect for passwords.
1304                 case '1':
1305                         $possible = "23456789bcdfghjkmnpqrstvwxyz";
1306                         break;
1307                 
1308                 // Same, with lower + upper
1309                 case '2':
1310                         $possible = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ";
1311                         break;
1312                 
1313                 // all letters, lowercase
1314                 case '3':
1315                         $possible = "abcdefghijklmnopqrstuvwxyz";
1316                         break;
1317                 
1318                 // all letters, lowercase + uppercase
1319                 case '4':
1320                         $possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1321                         break;
1322                 
1323                 // all digits & letters lowercase 
1324                 case '5':
1325                         $possible = "0123456789abcdefghijklmnopqrstuvwxyz";
1326                         break;
1327                 
1328                 // all digits & letters lowercase + uppercase
1329                 case '6':
1330                         $possible = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1331                         break;
1332                 
1333         }
1334
1335         $i = 0;
1336         while ($i < $length) {
1337                 $str .= substr($possible, mt_rand(0, strlen($possible)-1), 1);
1338                 $i++;
1339         }
1340         
1341         return yourls_apply_filter( 'rnd_string', $str, $length, $type, $charlist );
1342 }
1343
1344 /**
1345  * Return salted string
1346  *
1347  */
1348 function yourls_salt( $string ) {
1349         $salt = defined('YOURLS_COOKIEKEY') ? YOURLS_COOKIEKEY : md5(__FILE__) ;
1350         return yourls_apply_filter( 'yourls_salt', md5 ($string . $salt), $string );
1351 }
1352
1353 /**
1354  * Add a query var to a URL and return URL. Completely stolen from WP.
1355  * 
1356  * Works with one of these parameter patterns:
1357  *     array( 'var' => 'value' )
1358  *     array( 'var' => 'value' ), $url
1359  *     'var', 'value'
1360  *     'var', 'value', $url 
1361  * If $url ommited, uses $_SERVER['REQUEST_URI']
1362  *
1363  */
1364 function yourls_add_query_arg() {
1365         $ret = '';
1366         if ( is_array( func_get_arg(0) ) ) {
1367                 if ( @func_num_args() < 2 || false === @func_get_arg( 1 ) )
1368                         $uri = $_SERVER['REQUEST_URI'];
1369                 else
1370                         $uri = @func_get_arg( 1 );
1371         } else {
1372                 if ( @func_num_args() < 3 || false === @func_get_arg( 2 ) )
1373                         $uri = $_SERVER['REQUEST_URI'];
1374                 else
1375                         $uri = @func_get_arg( 2 );
1376         }
1377         
1378         $uri = str_replace( '&amp;', '&', $uri );
1379
1380         
1381         if ( $frag = strstr( $uri, '#' ) )
1382                 $uri = substr( $uri, 0, -strlen( $frag ) );
1383         else
1384                 $frag = '';
1385
1386         if ( preg_match( '|^https?://|i', $uri, $matches ) ) {
1387                 $protocol = $matches[0];
1388                 $uri = substr( $uri, strlen( $protocol ) );
1389         } else {
1390                 $protocol = '';
1391         }
1392
1393         if ( strpos( $uri, '?' ) !== false ) {
1394                 $parts = explode( '?', $uri, 2 );
1395                 if ( 1 == count( $parts ) ) {
1396                         $base = '?';
1397                         $query = $parts[0];
1398                 } else {
1399                         $base = $parts[0] . '?';
1400                         $query = $parts[1];
1401                 }
1402         } elseif ( !empty( $protocol ) || strpos( $uri, '=' ) === false ) {
1403                 $base = $uri . '?';
1404                 $query = '';
1405         } else {
1406                 $base = '';
1407                 $query = $uri;
1408         }
1409
1410         parse_str( $query, $qs );
1411         $qs = yourls_urlencode_deep( $qs ); // this re-URL-encodes things that were already in the query string
1412         if ( is_array( func_get_arg( 0 ) ) ) {
1413                 $kayvees = func_get_arg( 0 );
1414                 $qs = array_merge( $qs, $kayvees );
1415         } else {
1416                 $qs[func_get_arg( 0 )] = func_get_arg( 1 );
1417         }
1418
1419         foreach ( (array) $qs as $k => $v ) {
1420                 if ( $v === false )
1421                         unset( $qs[$k] );
1422         }
1423
1424         $ret = http_build_query( $qs );
1425         $ret = trim( $ret, '?' );
1426         $ret = preg_replace( '#=(&|$)#', '$1', $ret );
1427         $ret = $protocol . $base . $ret . $frag;
1428         $ret = rtrim( $ret, '?' );
1429         return $ret;
1430 }
1431
1432 /**
1433  * Navigates through an array and encodes the values to be used in a URL. Stolen from WP, used in yourls_add_query_arg()
1434  *
1435  */
1436 function yourls_urlencode_deep( $value ) {
1437         $value = is_array( $value ) ? array_map( 'yourls_urlencode_deep', $value ) : urlencode( $value );
1438         return $value;
1439 }
1440
1441 /**
1442  * Remove arg from query. Opposite of yourls_add_query_arg. Stolen from WP.
1443  *
1444  */
1445 function yourls_remove_query_arg( $key, $query = false ) {
1446         if ( is_array( $key ) ) { // removing multiple keys
1447                 foreach ( $key as $k )
1448                         $query = yourls_add_query_arg( $k, false, $query );
1449                 return $query;
1450         }
1451         return yourls_add_query_arg( $key, false, $query );
1452 }
1453
1454 /**
1455  * Return a time-dependent string for nonce creation
1456  *
1457  */
1458 function yourls_tick() {
1459         return ceil( time() / YOURLS_NONCE_LIFE );
1460 }
1461
1462 /**
1463  * Create a time limited, action limited and user limited token
1464  *
1465  */
1466 function yourls_create_nonce( $action, $user = false ) {
1467         if( false == $user )
1468                 $user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
1469         $tick = yourls_tick();
1470         return substr( yourls_salt($tick . $action . $user), 0, 10 );
1471 }
1472
1473 /**
1474  * Create a nonce field for inclusion into a form
1475  *
1476  */
1477 function yourls_nonce_field( $action, $name = 'nonce', $user = false, $echo = true ) {
1478         $field = '<input type="hidden" id="'.$name.'" name="'.$name.'" value="'.yourls_create_nonce( $action, $user ).'" />';
1479         if( $echo )
1480                 echo $field."\n";
1481         return $field;
1482 }
1483
1484 /**
1485  * Add a nonce to a URL. If URL omitted, adds nonce to current URL
1486  *
1487  */
1488 function yourls_nonce_url( $action, $url = false, $name = 'nonce', $user = false ) {
1489         $nonce = yourls_create_nonce( $action, $user );
1490         return yourls_add_query_arg( $name, $nonce, $url );
1491 }
1492
1493 /**
1494  * Check validity of a nonce (ie time span, user and action match).
1495  * 
1496  * Returns true if valid, dies otherwise (yourls_die() or die($return) if defined)
1497  * if $nonce is false or unspecified, it will use $_REQUEST['nonce']
1498  *
1499  */
1500 function yourls_verify_nonce( $action, $nonce = false, $user = false, $return = '' ) {
1501         // get user
1502         if( false == $user )
1503                 $user = defined( 'YOURLS_USER' ) ? YOURLS_USER : '-1';
1504                 
1505         // get current nonce value
1506         if( false == $nonce && isset( $_REQUEST['nonce'] ) )
1507                 $nonce = $_REQUEST['nonce'];
1508
1509         // what nonce should be
1510         $valid = yourls_create_nonce( $action, $user );
1511         
1512         if( $nonce == $valid ) {
1513                 return true;
1514         } else {
1515                 if( $return )
1516                         die( $return );
1517                 yourls_die( yourls__( 'Unauthorized action or expired link' ), yourls__( 'Error' ), 403 );
1518         }
1519 }
1520
1521 /**
1522  * Converts keyword into short link (prepend with YOURLS base URL)
1523  *
1524  */
1525 function yourls_link( $keyword = '' ) {
1526         $link = YOURLS_SITE . '/' . yourls_sanitize_keyword( $keyword );
1527         return yourls_apply_filter( 'yourls_link', $link, $keyword );
1528 }
1529
1530 /**
1531  * Converts keyword into stat link (prepend with YOURLS base URL, append +)
1532  *
1533  */
1534 function yourls_statlink( $keyword = '' ) {
1535         $link = YOURLS_SITE . '/' . yourls_sanitize_keyword( $keyword ) . '+';
1536         if( yourls_is_ssl() )
1537                 $link = str_replace( 'http://', 'https://', $link );
1538         return yourls_apply_filter( 'yourls_statlink', $link, $keyword );
1539 }
1540
1541 /**
1542  * Check if we're in API mode. Returns bool
1543  *
1544  */
1545 function yourls_is_API() {
1546         if ( defined( 'YOURLS_API' ) && YOURLS_API == true )
1547                 return true;
1548         return false;
1549 }
1550
1551 /**
1552  * Check if we're in Ajax mode. Returns bool
1553  *
1554  */
1555 function yourls_is_Ajax() {
1556         if ( defined( 'YOURLS_AJAX' ) && YOURLS_AJAX == true )
1557                 return true;
1558         return false;
1559 }
1560
1561 /**
1562  * Check if we're in GO mode (yourls-go.php). Returns bool
1563  *
1564  */
1565 function yourls_is_GO() {
1566         if ( defined( 'YOURLS_GO' ) && YOURLS_GO == true )
1567                 return true;
1568         return false;
1569 }
1570
1571 /**
1572  * Check if we're displaying stats infos (yourls-infos.php). Returns bool
1573  *
1574  */
1575 function yourls_is_infos() {
1576         if ( defined( 'YOURLS_INFOS' ) && YOURLS_INFOS == true )
1577                 return true;
1578         return false;
1579 }
1580
1581 /**
1582  * Check if we'll need interface display function (ie not API or redirection)
1583  *
1584  */
1585 function yourls_has_interface() {
1586         if( yourls_is_API() or yourls_is_GO() )
1587                 return false;
1588         return true;
1589 }
1590
1591 /**
1592  * Check if we're in the admin area. Returns bool
1593  *
1594  */
1595 function yourls_is_admin() {
1596         if ( defined( 'YOURLS_ADMIN' ) && YOURLS_ADMIN == true )
1597                 return true;
1598         return false;
1599 }
1600
1601 /**
1602  * Check if the server seems to be running on Windows. Not exactly sure how reliable this is.
1603  *
1604  */
1605 function yourls_is_windows() {
1606         return defined( 'DIRECTORY_SEPARATOR' ) && DIRECTORY_SEPARATOR == '\\';
1607 }
1608
1609 /**
1610  * Check if SSL is required. Returns bool.
1611  *
1612  */
1613 function yourls_needs_ssl() {
1614         if ( defined('YOURLS_ADMIN_SSL') && YOURLS_ADMIN_SSL == true )
1615                 return true;
1616         return false;
1617 }
1618
1619 /**
1620  * Return admin link, with SSL preference if applicable.
1621  *
1622  */
1623 function yourls_admin_url( $page = '' ) {
1624         $admin = YOURLS_SITE . '/admin/' . $page;
1625         if( yourls_is_ssl() or yourls_needs_ssl() )
1626                 $admin = str_replace('http://', 'https://', $admin);
1627         return yourls_apply_filter( 'admin_url', $admin, $page );
1628 }
1629
1630 /**
1631  * Return YOURLS_SITE or URL under YOURLS setup, with SSL preference
1632  *
1633  */
1634 function yourls_site_url( $echo = true, $url = '' ) {
1635         $url = yourls_get_relative_url( $url );
1636         $url = trim( YOURLS_SITE . '/' . $url, '/' );
1637         
1638         // Do not enforce (checking yourls_need_ssl() ) but check current usage so it won't force SSL on non-admin pages
1639         if( yourls_is_ssl() )
1640                 $url = str_replace( 'http://', 'https://', $url );
1641         $url = yourls_apply_filter( 'site_url', $url );
1642         if( $echo )
1643                 echo $url;
1644         return $url;
1645 }
1646
1647 /**
1648  * Check if SSL is used, returns bool. Stolen from WP.
1649  *
1650  */
1651 function yourls_is_ssl() {
1652         $is_ssl = false;
1653         if ( isset( $_SERVER['HTTPS'] ) ) {
1654                 if ( 'on' == strtolower( $_SERVER['HTTPS'] ) )
1655                         $is_ssl = true;
1656                 if ( '1' == $_SERVER['HTTPS'] )
1657                         $is_ssl = true;
1658         } elseif ( isset( $_SERVER['SERVER_PORT'] ) && ( '443' == $_SERVER['SERVER_PORT'] ) ) {
1659                 $is_ssl = true;
1660         }
1661         return yourls_apply_filter( 'is_ssl', $is_ssl );
1662 }
1663
1664
1665 /**
1666  * Get a remote page <title>, return a string (either title or url)
1667  *
1668  */
1669 function yourls_get_remote_title( $url ) {
1670         // Allow plugins to short-circuit the whole function
1671         $pre = yourls_apply_filter( 'shunt_get_remote_title', false, $url );
1672         if ( false !== $pre )
1673                 return $pre;
1674
1675         require_once( YOURLS_INC.'/functions-http.php' );
1676
1677         $url = yourls_sanitize_url( $url );
1678
1679         $title = $charset = false;
1680         
1681         $content = yourls_get_remote_content( $url );
1682         
1683         // If false, return url as title.
1684         // Todo: improve this with temporary title when shorturl_meta available?
1685         if( false === $content )
1686                 return $url;
1687
1688         if( $content !== false ) {
1689                 // look for <title>
1690                 if ( preg_match('/<title>(.*?)<\/title>/is', $content, $found ) ) {
1691                         $title = $found[1];
1692                         unset( $found );
1693                 }
1694
1695                 // look for charset
1696                 // <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
1697                 if ( preg_match('/<meta[^>]*?charset=([^>]*?)\/?>/is', $content, $found ) ) {
1698                         $charset = trim($found[1], '"\' ');
1699                         unset( $found );
1700                 }
1701         }
1702         
1703         // if title not found, guess if returned content was actually an error message
1704         if( $title == false && strpos( $content, 'Error' ) === 0 ) {
1705                 $title = $content;
1706         }
1707         
1708         if( $title == false )
1709                 $title = $url;
1710         
1711         /*
1712         if( !yourls_seems_utf8( $title ) )
1713                 $title = utf8_encode( $title );
1714         */
1715         
1716         // Charset conversion. We use @ to remove warnings (mb_ functions are easily bitching about illegal chars)
1717         if( function_exists( 'mb_convert_encoding' ) ) {
1718                 if( $charset ) {
1719                         $title = @mb_convert_encoding( $title, 'UTF-8', $charset );
1720                 } else {
1721                         $title = @mb_convert_encoding( $title, 'UTF-8' );
1722                 }
1723         }
1724         
1725         // Remove HTML entities
1726         $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' );
1727         
1728         // Strip out evil things
1729         $title = yourls_sanitize_title( $title );
1730         
1731         return yourls_apply_filter( 'get_remote_title', $title, $url );
1732 }
1733
1734 /**
1735  * Quick UA check for mobile devices. Return boolean.
1736  *
1737  */
1738 function yourls_is_mobile_device() {
1739         // Strings searched
1740         $mobiles = array(
1741                 'android', 'blackberry', 'blazer',
1742                 'compal', 'elaine', 'fennec', 'hiptop',
1743                 'iemobile', 'iphone', 'ipod', 'ipad',
1744                 'iris', 'kindle', 'opera mobi', 'opera mini',
1745                 'palm', 'phone', 'pocket', 'psp', 'symbian',
1746                 'treo', 'wap', 'windows ce', 'windows phone'
1747         );
1748         
1749         // Current user-agent
1750         $current = strtolower( $_SERVER['HTTP_USER_AGENT'] );
1751         
1752         // Check and return
1753         $is_mobile = ( str_replace( $mobiles, '', $current ) != $current );
1754         return yourls_apply_filter( 'is_mobile_device', $is_mobile );
1755 }
1756
1757 /**
1758  * Get request in YOURLS base (eg in 'http://site.com/yourls/abcd' get 'abdc')
1759  *
1760  */
1761 function yourls_get_request() {
1762         // Allow plugins to short-circuit the whole function
1763         $pre = yourls_apply_filter( 'shunt_get_request', false );
1764         if ( false !== $pre )
1765                 return $pre;
1766                 
1767         static $request = null;
1768
1769         yourls_do_action( 'pre_get_request', $request );
1770         
1771         if( $request !== null )
1772                 return $request;
1773         
1774         // Ignore protocol & www. prefix
1775         $root = str_replace( array( 'https://', 'http://', 'https://www.', 'http://www.' ), '', YOURLS_SITE );
1776         // Case insensitive comparison of the YOURLS root to match both http://Sho.rt/blah and http://sho.rt/blah
1777         $request = preg_replace( "!$root/!i", '', $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 1 );
1778
1779         // Unless request looks like a full URL (ie request is a simple keyword) strip query string
1780         if( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) {
1781                 $request = current( explode( '?', $request ) );
1782         }
1783         
1784         return yourls_apply_filter( 'get_request', $request );
1785 }
1786
1787 /**
1788  * Change protocol to match current scheme used (http or https)
1789  *
1790  */
1791 function yourls_match_current_protocol( $url, $normal = 'http', $ssl = 'https' ) {
1792         if( yourls_is_ssl() )
1793                 $url = str_replace( $normal, $ssl, $url );
1794         return yourls_apply_filter( 'match_current_protocol', $url );
1795 }
1796
1797 /**
1798  * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP.
1799  *
1800  */
1801 function yourls_fix_request_uri() {
1802
1803         $default_server_values = array(
1804                 'SERVER_SOFTWARE' => '',
1805                 'REQUEST_URI' => '',
1806         );
1807         $_SERVER = array_merge( $default_server_values, $_SERVER );
1808
1809         // Fix for IIS when running with PHP ISAPI
1810         if ( empty( $_SERVER['REQUEST_URI'] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER['SERVER_SOFTWARE'] ) ) ) {
1811
1812                 // IIS Mod-Rewrite
1813                 if ( isset( $_SERVER['HTTP_X_ORIGINAL_URL'] ) ) {
1814                         $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_ORIGINAL_URL'];
1815                 }
1816                 // IIS Isapi_Rewrite
1817                 else if ( isset( $_SERVER['HTTP_X_REWRITE_URL'] ) ) {
1818                         $_SERVER['REQUEST_URI'] = $_SERVER['HTTP_X_REWRITE_URL'];
1819                 } else {
1820                         // Use ORIG_PATH_INFO if there is no PATH_INFO
1821                         if ( !isset( $_SERVER['PATH_INFO'] ) && isset( $_SERVER['ORIG_PATH_INFO'] ) )
1822                                 $_SERVER['PATH_INFO'] = $_SERVER['ORIG_PATH_INFO'];
1823
1824                         // Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice)
1825                         if ( isset( $_SERVER['PATH_INFO'] ) ) {
1826                                 if ( $_SERVER['PATH_INFO'] == $_SERVER['SCRIPT_NAME'] )
1827                                         $_SERVER['REQUEST_URI'] = $_SERVER['PATH_INFO'];
1828                                 else
1829                                         $_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] . $_SERVER['PATH_INFO'];
1830                         }
1831
1832                         // Append the query string if it exists and isn't null
1833                         if ( ! empty( $_SERVER['QUERY_STRING'] ) ) {
1834                                 $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING'];
1835                         }
1836                 }
1837         }
1838 }
1839
1840 /**
1841  * Shutdown function, runs just before PHP shuts down execution. Stolen from WP
1842  *
1843  */
1844 function yourls_shutdown() {
1845         yourls_do_action( 'shutdown' );
1846 }
1847
1848 /**
1849  * Auto detect custom favicon in /user directory, fallback to YOURLS favicon, and echo/return its URL
1850  *
1851  */
1852 function yourls_favicon( $echo = true ) {
1853         static $favicon = null;
1854         if( $favicon !== null )
1855                 return $favicon;
1856         
1857         $custom = null;
1858         // search for favicon.(gif|ico|png|jpg|svg)
1859         foreach( array( 'gif', 'ico', 'png', 'jpg', 'svg' ) as $ext ) {
1860                 if( file_exists( YOURLS_USERDIR. '/favicon.' . $ext ) ) {
1861                         $custom = 'favicon.' . $ext;
1862                         break;
1863                 }
1864         }
1865         
1866         if( $custom ) {
1867                 $favicon = yourls_site_url( false, YOURLS_USERURL . '/' . $custom );
1868         } else {
1869                 $favicon = yourls_site_url( false ) . '/images/favicon.gif';
1870         }
1871         if( $echo )
1872                 echo $favicon;
1873         return $favicon;
1874 }
1875
1876 /**
1877  * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP.
1878  *
1879  */
1880 function yourls_check_maintenance_mode() {
1881
1882         $file = YOURLS_ABSPATH . '/.maintenance' ;
1883         if ( !file_exists( $file ) || yourls_is_upgrading() || yourls_is_installing() )
1884                 return;
1885         
1886         global $maintenance_start;
1887
1888         include( $file );
1889         // If the $maintenance_start timestamp is older than 10 minutes, don't die.
1890         if ( ( time() - $maintenance_start ) >= 600 )
1891                 return;
1892
1893         // Use any /user/maintenance.php file
1894         if( file_exists( YOURLS_USERDIR.'/maintenance.php' ) ) {
1895                 include( YOURLS_USERDIR.'/maintenance.php' );
1896                 die();
1897         }
1898         
1899         // https://www.youtube.com/watch?v=Xw-m4jEY-Ns
1900         $title   = yourls__( 'Service temporarily unavailable' );
1901         $message = yourls__( 'Our service is currently undergoing scheduled maintenance.' ) . "</p>\n<p>" .
1902         yourls__( 'Things should not last very long, thank you for your patience and please excuse the inconvenience' );
1903         yourls_die( $message, $title , 503 );
1904
1905 }
1906
1907 /**
1908  * Return current admin page, or null if not an admin page
1909  *
1910  * @return mixed string if admin page, null if not an admin page
1911  * @since 1.6
1912  */
1913 function yourls_current_admin_page() {
1914         if( yourls_is_admin() ) {
1915                 $current = substr( yourls_get_request(), 6 );
1916                 if( $current === false ) 
1917                         $current = 'index.php'; // if current page is http://sho.rt/admin/ instead of http://sho.rt/admin/index.php
1918                         
1919                 return $current;
1920         }
1921         return null;
1922 }
1923
1924 /**
1925  * Check if a URL protocol is allowed
1926  *
1927  * Checks a URL against a list of whitelisted protocols. Protocols must be defined with
1928  * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid
1929  * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either
1930  *
1931  * @since 1.6
1932  *
1933  * @param string $url URL to be check
1934  * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols
1935  * @return boolean true if protocol allowed, false otherwise
1936  */
1937 function yourls_is_allowed_protocol( $url, $protocols = array() ) {
1938         if( ! $protocols ) {
1939                 global $yourls_allowedprotocols;
1940                 $protocols = $yourls_allowedprotocols;
1941         }
1942         
1943         $protocol = yourls_get_protocol( $url );
1944         return yourls_apply_filter( 'is_allowed_protocol', in_array( $protocol, $protocols ), $url, $protocols );
1945 }
1946
1947 /**
1948  * Get protocol from a URL (eg mailto:, http:// ...)
1949  *
1950  * @since 1.6
1951  *
1952  * @param string $url URL to be check
1953  * @return string Protocol, with slash slash if applicable. Empty string if no protocol
1954  */
1955 function yourls_get_protocol( $url ) {
1956         preg_match( '!^[a-zA-Z0-9\+\.-]+:(//)?!', $url, $matches );
1957         /*
1958         http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
1959         The scheme name consists of a sequence of characters beginning with a letter and followed by any
1960         combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are
1961         case-insensitive, the canonical form is lowercase and documents that specify schemes must do so
1962         with lowercase letters. It is followed by a colon (":").
1963         */
1964         $protocol = ( isset( $matches[0] ) ? $matches[0] : '' );
1965         return yourls_apply_filter( 'get_protocol', $protocol, $url );
1966 }
1967
1968 /**
1969  * Get relative URL (eg 'abc' from 'http://sho.rt/abc')
1970  *
1971  * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is
1972  * or return empty string if $strict is true
1973  *
1974  * @since 1.6
1975  * @param string $url URL to relativize
1976  * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string
1977  * @return string URL 
1978  */
1979 function yourls_get_relative_url( $url, $strict = true ) {
1980         $url = yourls_sanitize_url( $url );
1981
1982         // Remove protocols to make it easier
1983         $noproto_url  = str_replace( 'https:', 'http:', $url );
1984         $noproto_site = str_replace( 'https:', 'http:', YOURLS_SITE );
1985         
1986         // Trim URL from YOURLS root URL : if no modification made, URL wasn't relative
1987         $_url = str_replace( $noproto_site . '/', '', $noproto_url );
1988         if( $_url == $noproto_url )
1989                 $_url = ( $strict ? '' : $url );
1990
1991         return yourls_apply_filter( 'get_relative_url', $_url, $url );
1992 }
1993
1994 /**
1995  * Marks a function as deprecated and informs when it has been used. Stolen from WP.
1996  *
1997  * There is a hook deprecated_function that will be called that can be used
1998  * to get the backtrace up to what file and function called the deprecated
1999  * function.
2000  *
2001  * The current behavior is to trigger a user error if YOURLS_DEBUG is true.
2002  *
2003  * This function is to be used in every function that is deprecated.
2004  *
2005  * @since 1.6
2006  * @uses yourls_do_action() Calls 'deprecated_function' and passes the function name, what to use instead,
2007  *   and the version the function was deprecated in.
2008  * @uses yourls_apply_filters() Calls 'deprecated_function_trigger_error' and expects boolean value of true to do
2009  *   trigger or false to not trigger error.
2010  *
2011  * @param string $function The function that was called
2012  * @param string $version The version of WordPress that deprecated the function
2013  * @param string $replacement Optional. The function that should have been called
2014  */
2015 function yourls_deprecated_function( $function, $version, $replacement = null ) {
2016
2017         yourls_do_action( 'deprecated_function', $function, $replacement, $version );
2018
2019         // Allow plugin to filter the output error trigger
2020         if ( YOURLS_DEBUG && yourls_apply_filters( 'deprecated_function_trigger_error', true ) ) {
2021                 if ( ! is_null( $replacement ) )
2022                         trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) );
2023                 else
2024                         trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.'), $function, $version ) );
2025         }
2026 }
2027
2028 /**
2029  * Return the value if not an empty string
2030  *
2031  * Used with array_filter(), to remove empty keys but not keys with value 0 or false
2032  *
2033  * @since 1.6
2034  * @param mixed $val Value to test against ''
2035  * @return bool True if not an empty string
2036  */
2037 function yourls_return_if_not_empty_string( $val ) {
2038         return( $val !== '' );
2039 }