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