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