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