3 Copyright (c) 2009, Beau Lebens
6 Redistribution and use in source and binary forms, with or without modification,
7 are permitted provided that the following conditions are met:
9 - Redistributions of source code must retain the above copyright notice, this
10 list of conditions and the following disclaimer.
11 - Redistributions in binary form must reproduce the above copyright notice,
12 this list of conditions and the following disclaimer in the documentation
13 and/or other materials provided with the distribution.
14 - Neither the name of Dented Reality nor the names of the authors may be used
15 to endorse or promote products derived from this software without specific
16 prior written permission.
18 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 // Return options for Slinky_Service->url_get() and ->url_post()
32 define( 'SLINKY_BODY', 1 ); // Default
33 define( 'SLINKY_HEADERS', 2 ); // Not implemented yet
34 define( 'SLINKY_FINAL_URL', 3 ); // Default for lengthening URLs
36 // So that services may decide what to do with us
37 define( 'SLINKY_USER_AGENT', 'Slinky v1.0 +http://dentedreality.com.au/projects/slinky/' );
39 // How many seconds until remote requests should be cut?
40 define( 'SLINKY_TIMEOUT', 10 );
43 * Slinky allows you to go back and forth between "long" and shortened URLs
44 * using popular URL shortening services.
46 * Slinky assumes you have cURL installed and working, and requires the JSON
47 * extension installed if you're working with a service that uses JSON.
49 * Slinky will ONLY work with PHP5+
51 * It supports some of the more popular services, with easy extensibility for
52 * adding your own relatively easily. It defaults to using TinyURL
53 * for shortening URLs. If you want to use some of the other services, you need
54 * to set some configuration options before actually shortening/lengthening
55 * URLs. I'd strongly suggest that you cache results using memcached, a local
56 * DB or whatever to avoid having to hit APIs etc every time you encounter a
59 * Slinky supports shortening, and auto-detection (for lengthening URLs)
60 * using these services:
71 * - Snipr / Snipurl / Snurl.com / Sn.im
76 * $slinky = new Slinky( 'http://dentedreality.com.au/' );
77 * - Creates a new Slinky instance, will default to using TinyURL for ->short();
79 * $slinky = new Slinky( 'http://dentedreality.com.au', new Slinky_Bitly() );
80 * - Creates new Slinky, forces use of Bit.ly for ->short();
82 * $slinky = new Slinky( 'http://dentedreality.com.au/' );
83 * echo $slinky->short();
84 * - echos the short version of http://dentedreality.com.au/ (default to TinyURL)
86 * $slinky = new Slinky( 'http://tinyurl.com/jw5sh' );
87 * echo $slinky->long();
88 * - echos out the long version of http://tinyurl.com/jw5sh (which should be http://dentedreality.com.au/)
90 * $slinky = new Slinky( 'http://dentedreality.com.au/' );
91 * echo $slinky->long();
92 * - Attempts to lengthen the URL, but will not auto-detect which service it is
93 * so it will just output the original URL. Useful for always attempting to
94 * lengthen any URL you come across (fails gracefully)
96 * $slinky = new Slinky( 'http://dentedreality.com.au/' );
97 * $slinky->set_cascade( array( new Slinky_Trim(), new Slinky_IsGd(), new Slinky_TinyURL() ) );
98 * echo $slinky->short();
99 * - Uses the powerful cascade mode to make sure that we get a short URL from
100 * Tr.im, Is.Gd or TinyURL (in that order).
102 * See specific service class definitions below for examples of how to use them,
103 * as some services allow (or require) additional properties before you can use
104 * them (for authentication etc).
106 * To use a different service with Slinky, just create your own class and
107 * extend Slinky_Service(). Make sure you implement url_is_short(), url_is_long(),
108 * make_short() and make_long(). If you need to GET or POST a URL, you can use
109 * ->url_get() and ->url_post(), which your class will have inherited.
113 var $service = false;
114 var $cascade = false;
116 function __construct( $url = false, $service = false ) {
118 $this->service = $service;
122 * Specify which URL Service to use
124 * @param Slinky_Service $service Packaged or custom Service object
127 public function set_service( $service = false ) {
128 if ( is_object( $service ) ) {
129 $this->service = $service;
134 * If you pass an array of Slinky_Service objects to this method, they will
135 * be used in order to try to get a short URL, so if one fails, it will
136 * try the next and so on, until it gets a valid short URL, or it runs
139 * @param array $services List of Slinky_Service objects as an array
141 public function set_cascade( $services = false ) {
142 if ( !$services || !is_array( $services ) )
145 $this->cascade = $services;
149 * Guess the URL service to use from known domains of short URLs
153 public function set_service_from_url( $url = false ) {
157 $host = parse_url( $url, PHP_URL_HOST );
158 switch ( str_replace( 'www.', '', $host ) ) {
160 if ( class_exists( 'Slinky_Bitly' ) ) {
161 $this->service = new Slinky_Bitly();
165 if ( class_exists( 'Slinky_Trim' ) ) {
166 $this->service = new Slinky_Trim();
170 if ( class_exists( 'Slinky_TinyURL' ) ) {
171 $this->service = new Slinky_TinyURL();
175 if ( class_exists( 'Slinky_IsGd' ) ) {
176 $this->service = new Slinky_IsGd();
180 if ( class_exists( 'Slinky_Fongs' ) ) {
181 $this->service = new Slinky_Fongs();
184 case $this->get( 'yourls-url' ):
185 if ( class_exists( 'Slinky_YourLS' ) ) {
186 $this->service = new Slinky_YourLS();
190 if ( class_exists( 'Slinky_Micurl' ) ) {
191 $this->service = new Slinky_Micurl();
195 if ( class_exists( 'Slinky_Ur1ca' ) ) {
196 $this->service = new Slinky_Ur1ca();
200 if ( class_exists( 'Slinky_PtitURL' ) ) {
201 $this->service = new Slinky_PtitURL();
206 if ( class_exists( 'Slinky_TightURL' ) ) {
207 $this->service = new Slinky_TightURL();
214 if ( class_exists( 'Slinky_Snipr' ) ) {
215 $this->service = new Slinky_Snipr();
219 $this->service = new Slinky_Default();
225 * Take a long URL and make it short. Will avoid "re-shortening" a URL if it
226 * already seems to be short.
228 * @param string $url Optional URL to shorten, otherwise use $this->url
229 * @return The short version of the URL
231 public function short( $url = false ) {
235 if ( !$this->service )
236 $this->set_service( new Slinky_TinyURL() ); // Defaults to tinyurl because it doesn't require any configuration
238 if ( !$this->cascade )
239 $this->cascade = array( $this->service ); // Use a single service in cascade mode
241 foreach ( $this->cascade as $service ) {
242 if ( $service->url_is_short( $this->url ) )
243 return trim( $this->url ); // Identified as already short, using this service
245 $response = trim( $service->make_short( $this->url ) );
246 if ( $response && $this->url != $response )
247 return trim( $response );
250 return $this->url; // If all else fails, just send back the URL we already know about
254 * Take a short URL and make it long ("resolve" it).
256 * @param string $url The short URL
259 public function long( $url = false ) {
263 if ( !$this->service )
264 $this->set_service_from_url();
266 if ( $this->service->url_is_long( $this->url ) )
267 return trim( $this->url );
269 return trim( $this->service->make_long( $this->url ) );
274 * Use this class to create a Service implementation for your own URL
275 * shortening service. Extend the class and customize methods to suit your
276 * service. Note that it is an "abstract" class, so there are certain methods
277 * which you *must* define.
279 abstract class Slinky_Service {
282 * Determine, based on the input URL, if it's already a short URL, from
283 * this particular service. e.g. a Bit.ly URL contains "bit.ly/"
285 abstract function url_is_short( $url );
288 * Determine if this is a "long" URL (just means it hasn't been shortened)
289 * already. e.g. a no-Bit.ly URL would NOT contain "bit.ly/"
291 abstract function url_is_long( $url );
294 * Handle taking the $url and converting it to a short URL via whatever
295 * means is provided at the remote service.
297 abstract function make_short( $url );
300 * Return the long/expanded version of a URL via any API means available
301 * from this service. As a fallback, you might
302 * consider just following the URL and using SLINKY_FINAL_URL as the
303 * return method from a $this->url_get() call to find out.
305 * This one is optional for Services extending this class, if they don't
306 * then the following implementation will work on most services anyway.
308 public function make_long( $url ) {
309 return $this->url_get( $url, SLINKY_FINAL_URL );
313 * Method for getting properties that you might need during the process
314 * of shortening/lengthening a URL (e.g. auth credentials)
316 public function get( $prop ) {
317 if ( empty( $this->$prop ) )
324 * Method for setting properties that you might need during the process
325 * of shortening/lengthening a URL (e.g. auth credentials)
327 public function set( $prop, $val ) {
332 * Internal helper for performing a GET request on a remote URL.
334 * @param string $url The URL to GET
335 * @param const $return The return method [ SLINKY_BODY | SLINKY_FINAL_URL | SLINKY_HEADERS ]
336 * @return Mixed, based on the $return var passed in.
338 protected function url_get( $url, $return = SLINKY_BODY ) {
339 $ch = curl_init( $url );
340 curl_setopt( $ch, CURLOPT_USERAGENT, SLINKY_USER_AGENT );
341 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 ); // Don't stress about SSL validity
342 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); // Return the response, don't output it
343 curl_setopt( $ch, CURLOPT_TIMEOUT, SLINKY_TIMEOUT ); // Limit how long we'll wait for a response
344 //curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); // Allow following of redirections
345 $r = curl_exec( $ch );
346 if ( curl_errno( $ch ) ) {
350 // Return whatever we were asked for
351 if ( SLINKY_FINAL_URL == $return )
352 return curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
353 else if ( SLINKY_BODY == $return )
360 * Internal helper for performing a POST request on a remote URL.
362 * @param string $url The URL to POST to
363 * @param array $payload Array containing name/value pairs of the parameters to POST
364 * @param const $return The return method [ SLINKY_BODY | SLINKY_FINAL_URL | SLINKY_HEADERS ]
365 * @return Mixed, based on the $return var passed in.
367 protected function url_post( $url, $payload = array(), $return = SLINKY_BODY ) {
368 $ch = curl_init( $url );
369 curl_setopt( $ch, CURLOPT_POST, true );
370 curl_setopt( $ch, CURLOPT_POSTFIELDS, (array) $payload );
371 curl_setopt( $ch, CURLOPT_USERAGENT, SLINKY_USER_AGENT );
372 curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 ); // Don't stress about SSL validity
373 curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); // Return the response, don't output it
374 curl_setopt( $ch, CURLOPT_TIMEOUT, SLINKY_TIMEOUT ); // Limit how long we'll wait for a response
375 //curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); // Allow following of redirections
376 $r = curl_exec( $ch );
377 if ( curl_errno( $ch ) ) {
381 // Return whatever we were asked for
382 if ( SLINKY_FINAL_URL == $return )
383 return curl_getinfo( $ch, CURLINFO_EFFECTIVE_URL );
384 else if ( SLINKY_BODY == $return )
391 // This default service is used in cases when you try to do something based
392 // on auto-detection, but we can't detect anything. It will also resolve URLs
393 // to their "long" version by following all redirects.
394 class Slinky_Default extends Slinky_Service {
395 function url_is_short( $url ) {
399 function url_is_long( $url ) {
403 function make_short( $url ) {
408 // Implementation of TinyURL as a Slinky Service
409 class Slinky_TinyURL extends Slinky_Service {
410 function url_is_short( $url ) {
411 return stristr( $url, 'tinyurl.com/' );
414 function url_is_long( $url ) {
415 return !stristr( $url, 'tinyurl.com/' );
418 function make_short( $url ) {
419 return $this->url_get( 'http://tinyurl.com/api-create.php?url=' . urlencode( $url ) );
422 function make_long( $url ) {
423 $bits = parse_url( $url );
424 $result = $this->url_get( 'http://tinyurl.com/preview.php?num=' . substr( $bits['path'], 1 ) );
425 if ( preg_match('/<a id="redirecturl" href="([^"]+)">/is', $result, $matches ) )
432 // Implementation of Bit.ly as a Slinky Service
434 To use Bit.ly, you MUST set your login and apiKey for the service first, e.g.
436 $bitly = new Slinky_Bitly();
437 $bitly->set( 'login', 'bitly_login' );
438 $bitly->set( 'apiKey', 'bitly_apiKey' );
440 $slinky = new Slinky( $url, $bitly );
441 echo $slinky->short();
443 You could also do this if the URL was already a bit.ly URL and you
444 were going to make it longer, since Bitly is supported for auto-detection:
446 $slinky = new Slinky( $url );
447 $slinky->set_service_from_url();
448 $slinky->service->set( 'login', 'bitly_login' );
449 $slinky->service->set( 'apiKey', 'bitly_apiKey' );
450 echo $slinky->long();
453 class Slinky_Bitly extends Slinky_Service {
454 function url_is_short( $url ) {
455 return stristr( $url, 'bit.ly/' );
458 function url_is_long( $url ) {
459 return !stristr( $url, 'bit.ly/' );
462 function make_short( $url ) {
463 // Can't do anything unless these 2 properties are set first
464 if ( !$this->get( 'login' ) || !$this->get( 'apiKey' ) )
467 $result = $this->url_post( 'http://api.bit.ly/shorten?version=2.0.1&format=json&login=' . $this->get( 'login' ) . '&apiKey=' . $this->get( 'apiKey' ) . '&longUrl=' . urlencode( $url ) );
468 $result = json_decode( $result );
469 if ( !$result->errorCode ) {
470 foreach ( $result->results as $detail ) {
471 return $detail->shortUrl;
478 function make_long( $url ) {
479 // Can't do anything unless these 2 properties are set first
480 if ( !$this->get( 'login' ) || !$this->get( 'apiKey' ) )
483 $result = $this->url_post( 'http://api.bit.ly/expand?version=2.0.1&format=json&login=' . $this->get( 'login' ) . '&apiKey=' . $this->get( 'apiKey' ) . '&shortUrl=' . urlencode( $url ) );
484 $result = json_decode( $result );
485 if ( !$result->errorCode ) {
486 foreach ( $result->results as $detail ) {
487 return $detail->longUrl;
495 // Implementation of Tr.im as a Slinky Service
497 When using Tr.im, you MAY optionally set your username and password to tie
498 URLs to your account, e.g.
500 $trim = new Slinky_Trim();
501 $trim->set( 'username', 'trim_username' );
502 $trim->set( 'password', 'trim_password' );
504 $slinky = new Slinky( $url, $trim );
505 echo $slinky->short();
507 You could also do this if the URL was already a tr.im URL and you
508 were going to make it longer, since Tr.im is supported for auto-detection:
510 $slinky = new Slinky( $url );
511 $slinky->set_service_from_url();
512 echo $slinky->long();
515 class Slinky_Trim extends Slinky_Service {
516 function url_is_short( $url ) {
517 return stristr( $url, 'tr.im/' );
520 function url_is_long( $url ) {
521 return !stristr( $url, 'tr.im/' );
524 function make_short( $url ) {
525 $url = 'http://api.tr.im/api/trim_simple?url=' . urlencode( $url );
527 if ( $this->get( 'username' ) && $this->get( 'password' ) )
528 $url .= '&username=' . urlencode( $this->get( 'username' ) ) . '&password=' . urlencode( $this->get( 'password' ) );
530 return $this->url_get( $url );
533 function make_long( $url ) {
534 $bits = parse_url( $url );
535 $result = $this->url_get( 'http://api.tr.im/api/trim_destination.json?trimpath=' . substr( $bits['path'], 1 ) );
536 $result = json_decode($result);
537 if ( 'OK' == $result->status->result )
538 return $result->destination;
544 // Implementation of Is.Gd as a Slinky Service
545 class Slinky_IsGd extends Slinky_Service {
546 function url_is_short( $url ) {
547 return stristr( $url, 'is.gd/' );
550 function url_is_long( $url ) {
551 return !stristr( $url, 'is.gd/' );
554 function make_short( $url ) {
555 $response = $this->url_get( 'http://is.gd/api.php?longurl=' . urlencode( $url ) );
556 if ( 'error' == substr( strtolower( $response ), 0, 5 ) )
564 class Slinky_Fongs extends Slinky_Service {
565 function url_is_short( $url ) {
566 return stristr( $url, 'fon.gs/' );
569 function url_is_long( $url ) {
570 return !stristr( $url, 'fon.gs/' );
573 function make_short( $url ) {
574 $response = $this->url_get( 'http://fon.gs/create.php?url=' . urlencode( $url ) );
575 if ( 'OK:' == substr( $response, 0, 3 ) )
576 return str_replace( 'OK: ', '', $response );
583 class Slinky_YourLS extends Slinky_Service {
584 function url_is_short( $url ) {
585 return stristr( $url, 'shit.li/' );
588 function url_is_long( $url ) {
589 return !stristr( $url, 'shit.li/' );
592 function make_short( $url ) {
593 echo $this->get( 'username' );
594 $use_ssl = $this->get( 'ssl' );
599 $result = $this->url_get( 'http'. $use_ssl . '://' . $this->get( 'yourls-url' ) . '/yourls-api.php?username=' . $this->get( 'username' ) . '&password=' . $this->get( 'password' ) . '&action=shorturl&format=simple&url=' . urlencode( $url ) );
600 if ( 1 != $result && 2 != $result )
608 class Slinky_Micurl extends Slinky_Service {
609 function url_is_short( $url ) {
610 return stristr( $url, 'micurl.com/' );
613 function url_is_long( $url ) {
614 return !stristr( $url, 'micurl.com/' );
617 function make_short( $url ) {
618 $result = $this->url_get( 'http://micurl.com/api.php?url=' . urlencode( $url ) );
619 if ( 1 != $result && 2 != $result )
620 return 'http://micurl.com/' . $result;
627 class Slinky_Ur1ca extends Slinky_Service {
628 function url_is_short( $url ) {
629 return stristr( $url, 'ur1.ca/' );
632 function url_is_long( $url ) {
633 return !stristr( $url, 'ur1.ca/' );
636 function make_short( $url ) {
637 $result = $this->url_post( 'http://ur1.ca/', array( 'longurl' => $url ) );
638 if ( preg_match( '/<p class="success">Your ur1 is: <a href="([^"]+)">/', $result, $matches ) )
646 class Slinky_PtitURL extends Slinky_Service {
647 function url_is_short( $url ) {
648 return stristr( $url, 'ptiturl.com/' );
651 function url_is_long( $url ) {
652 return !stristr( $url, 'ptiturl.com/' );
655 function make_short( $url ) {
656 $result = $this->url_get( 'http://ptiturl.com/index.php?creer=oui&url=' . urlencode( $url ) );
657 if ( preg_match( '/<pre><a href=\'?([^\'>]+)\'?>/', $result, $matches ) )
665 class Slinky_TightURL extends Slinky_Service {
666 function url_is_short( $url ) {
667 return stristr( $url, 'tighturl.com/' )
668 || stristr( $url, '2tu.us/' );
671 function url_is_long( $url ) {
672 return !stristr( $url, 'tighturl.com/' )
673 && !stristr( $url, '2tu.us/' );
676 function make_short( $url ) {
677 $response = $this->url_get( 'http://tighturl.com/?save=y&url=' . urlencode( $url ) );
678 if ( preg_match( '/Your tight URL is: <code><a href=\'([^\']+)\' target=\'_blank\'>/', $response, $matches ) ) {
688 To use Snipr, you MUST set your user_id and API (key) for the service first, e.g.
690 $snipr = new Slinky_Snipr();
691 $snipr->set( 'user_id', 'Snipr User ID' );
692 $snipr->set( 'API', 'Snipr API Key' );
694 $slinky = new Slinky( $url, $snipr );
695 echo $slinky->short();
697 NOTE: Snipr requires the SimpleXML extension to be installed for lengthening URLs
699 class Slinky_Snipr extends Slinky_Service {
700 // Snipurl, Snurl, Snipr, Sn.im
701 function url_is_short( $url ) {
702 return stristr( $url, 'snipr.com/' ) || stristr( $url, 'snipurl.com/' ) || stristr( $url, 'snurl.com/' ) || stristr( $url, 'sn.im/' );
705 function url_is_long( $url ) {
706 return !stristr( $url, 'snipr.com/' ) || !stristr( $url, 'snipurl.com/' ) || !stristr( $url, 'snurl.com/' ) || !stristr( $url, 'sn.im/' );
709 function make_short( $url ) {
710 if ( !$this->get( 'user_id' ) || !$this->get( 'API' ) )
713 $response = $this->url_post( 'http://snipr.com/site/getsnip', array( 'sniplink' => urlencode( $url ), 'snipuser' => $this->get( 'user_id'), 'snipapi' => $this->get( 'API' ), 'snipformat' => 'simple' ) );
714 if ( 'ERROR' != substr( $response, 0, 5 ) )
721 // If you're testing things out, http://dentedreality.com.au/ should convert to:
722 // - http://tinyurl.com/jw5sh
723 // - http://bit.ly/hEkAD
724 // - http://tr.im/sk1H
725 // - http://is.gd/1yJ81
726 // - http://fon.gs/tc1p8c
727 // - http://micurl.com/qen3uub
728 // - http://ur1.ca/7dcd
729 // - http://ptiturl.com/?id=bac8fb
730 // - http://tighturl.com/kgd
731 // - http://snipr.com/nbbw3
733 // $slinky = new Slinky( 'http://dentedreality.com.au/' );
734 // echo $slinky->short();