/mvc/models/shorturl
[return to app]1
<?php
2 /**
3 * Replace long URLs with short URLs that are easy to type and dictate
4 */
5 class shorturlModel extends model {
6 /**
7 * Table/collection name - only need to change this if you will be operating multiple Vork account instances
8 * on the same database
9 * @var string
10 */
11 protected $_table = 'shorturls';
12
13 /**
14 * Database type to use
15 * @var string Either sql or mongo
16 */
17 protected $_db;
18
19 /**
20 * Mongo collection object cache
21 * @var MongoCollection
22 */
23 protected $_mongo;
24
25 /**
26 * Sets $this->_db and if it is Mongo then it sets the MongoDB collection
27 */
28 public function __construct() {
29 $this->_db = (!in_array('mongo', config::$modelObjects) ? 'sql' : 'mongo');
30 }
31
32 /**
33 * Switchboard to route to either SQL or Mongo methods
34 *
35 * @param string $name
36 * @param array $args
37 */
38 public function __call($name, array $args) {
39 $name = '_' . $this->_db . ucfirst($name);
40 if ($this->_db == 'mongo' && !$this->_mongo) {
41 $this->_mongo = $this->mongo->selectCollection($this->_table);
42 }
43 return call_user_func_array(array($this, $name), $args);
44 }
45
46 /**
47 * Retrieve the full URL for a short URL
48 *
49 * @param string $shortUrl
50 * @return string Returns false if not found
51 */
52 protected function _sqlGetShortUrl($shortUrl) {
53 $sql = 'select url from ' . $this->_table . ' where shorturl=' . $this->db->cleanString($shortUrl);
54 $res = $this->db->query($sql);
55 $row = $res->fetch_row();
56 return ($row ? current($row) : $row);
57 }
58 protected function _mongoGetShortUrl($shortUrl) {
59 $row = $this->_mongo->findOne(array('_id' => $shortUrl));
60 return ($row ? $row['url'] : $row);
61 }
62
63 /**
64 * Log the forwarding action
65 * @param string $shortUrl
66 */
67 protected function _sqlLogForward($shortUrl) {
68 $args['table'] = $this->_table . '_log';
69 $args['vals']['shorturl'] = $this->db->cleanString($shortUrl);
70 if (isset($_SERVER['HTTP_REFERER'])) {
71 $args['vals']['referrer'] = $this->db->cleanString($_SERVER['HTTP_REFERER']);
72 }
73 if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
74 $args['vals']['ip'] = $this->_sqlIp($_SERVER['REMOTE_ADDR']);
75 }
76 $args['vals']['occurrence'] = 'current_timestamp';
77 $sql = $this->db->insertSql($args);
78 $this->db->query($sql);
79 }
80 protected function _mongoLogForward($shortUrl) {
81 $args = array('shorturl' => $shortUrl, 'occurrence' => time());
82 if (isset($_SERVER['HTTP_REFERER'])) {
83 $args['referrer'] = $_SERVER['HTTP_REFERER'];
84 }
85 if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
86 $args['ip'] = $_SERVER['REMOTE_ADDR'];
87 }
88 return $this->mongo->selectCollection($this->_table . '_log')->insert($args);
89 }
90
91 /**
92 * Adds a new short URL
93 *
94 * If a URL already has been shortened then this method will return the original shortUrl, it will not
reshorten it.
95 *
96 * @param string $url
97 * @return string
98 */
99 protected function _sqlAddShortUrl($url) {
100 if (substr($url, -1) == '/') { //strip trailing slash for URL consistency
101 $url = substr($url, 0, -1);
102 }
103 $sql = 'select shorturl from ' . $this->_table . ' where url=' . $this->db->cleanString($url);
104 if (!$res = $this->db->query($sql)) {
105 $this->_initializeDatabase();
106 $res = $this->db->query($sql);
107 }
108 $row = $res->fetch_row();
109 if ($row) { //if URL is already in the database just retrun the existing shortUrl
110 $shortUrl = current($row);
111 } else {
112 $shortUrl = $this->_generateShortUrl();
113 $args['table'] = $this->_table;
114 $quote = $this->db->surroundingQuote;
115 $args['vals'] = array('shorturl' => $quote . $shortUrl . $quote, 'added' => 'current_timestamp',
116 'url' => $this->db->cleanString($url));
117 if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
118 $args['vals']['ip'] = $this->_sqlIp($_SERVER['REMOTE_ADDR']);
119 }
120 $sql = $this->db->insertSql($args);
121 $this->db->query($sql);
122 }
123 return $shortUrl;
124 }
125 protected function _mongoAddShortUrl($url) {
126 if (substr($url, -1) == '/') { //strip trailing slash for URL consistency
127 $url = substr($url, 0, -1);
128 }
129 $row = $this->_mongo->findOne(array('url' => $url));
130 if ($row) { //if URL is already in the database just retrun the existing shortUrl
131 $shortUrl = $row['_id'];
132 } else {
133 $shortUrl = $this->_generateShortUrl();
134 $args = array('_id' => $shortUrl, 'added' => time(), 'url' => $url);
135 if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)) {
136 $args['ip'] = $_SERVER['REMOTE_ADDR'];
137 }
138 $this->_mongo->save($args);
139 $this->_mongo->ensureIndex(array('url' => true), array('unique' => true));
140 }
141 return $shortUrl;
142 }
143
144 /**
145 * Generates a 4-digit alphanumeric shortUrl consisting of easily-differentiated characters only
146 * @return string
147 */
148 protected function _generateShortUrl() {
149 //only letters that are easy to visually differentiate
150 $chars = array('b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'm', 'n',
151 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z');
152 $alphanums = array_merge($chars, range(2, 9));
153 $charset = array_rand($alphanums, 4); //28 chars to the power of 4 = 614,656 combinations
154 $shortUrl = $alphanums[$charset[0]] . $alphanums[$charset[1]]
155 . $alphanums[$charset[2]] . $alphanums[$charset[3]];
156 if ($this->getShortUrl($shortUrl)) { //that shorturl already exists, generate another
157 $shortUrl = $this->_generateShortUrl();
158 }
159 return $shortUrl;
160 }
161
162 /**
163 * Wrap the IP address for inserting into SQL - for MySQL this will also convert A-to-N
164 *
165 * @param string $ip
166 * @return string
167 */
168 protected function _sqlIp($ip) {
169 $dbParents = class_parents($this->db);
170 $isMysql = (isset($dbParents['mysqli']) || isset($dbParents['mysql']));
171 $quote = $this->db->surroundingQuote;
172 return ($isMysql ? 'inet_aton(' . $quote . $ip . $quote . ')' : $this->db->cleanString($ip));
173
174 }
175
176 /**
177 * Creates the database tables
178 *
179 * If your database does not support varchars over 255 chars or the datetime column type then you will need
180 * to adjust the data types used in this method. Oracle users may also swap varchar with varchar2.
181 */
182 protected function _initializeDatabase() {
183 $dbParents = class_parents($this->db);
184 $isMysql = (isset($dbParents['mysqli']) || isset($dbParents['mysql']));
185 $sql = 'create table ' . $this->_table . ' (
186 shorturl char(4) not null primary key,
187 url varchar(765) not null unique,
188 added datetime not null,
189 ip ' . ($isMysql ? 'int(10) unsigned' : 'varchar(16)') . ' null)';
190 $this->db->query($sql);
191
192 $sql = 'create table ' . $this->_table . '_log (
193 shorturl char(4) not null,
194 referrer varchar(255) null,
195 occurrence datetime not null,
196 ip ' . ($isMysql ? 'int(10) unsigned' : 'varchar(16)') . ' null)';
197 if ($isMysql) {
198 $sql .= ' engine=archive';
199 }
200 $this->db->query($sql);
201 }
202 }