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