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