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