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