• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-server / 6cc846a8-d6e0-4d0e-ab80-b31da8ff7ecf

18 Aug 2025 03:06PM UTC coverage: 90.406% (-0.003%) from 90.409%
6cc846a8-d6e0-4d0e-ab80-b31da8ff7ecf

push

circleci

edwh
PHP test refactoring.

25942 of 28695 relevant lines covered (90.41%)

31.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.98
/include/misc/Shortlink.php
1
<?php
2
namespace Freegle\Iznik;
3

4

5

6
class Shortlink extends Entity
7
{
8
    /** @var  $dbhm LoggedPDO */
9
    var $publicatts = array('id', 'name', 'type', 'groupid', 'url', 'clicks', 'created');
10
    var $settableatts = array('name');
11

12
    const TYPE_GROUP = 'Group';
13
    const TYPE_OTHER = 'Other';
14

15
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm, $id = NULL)
16
    {
17
        $this->fetch($dbhr, $dbhm, $id, 'shortlinks', 'shortlink', $this->publicatts);
385✔
18
    }
19

20
    public function create($name, $type, $groupid = NULL, $url = NULL) {
21
        $ret = NULL;
346✔
22

23
        $rc = $this->dbhm->preExec("INSERT INTO shortlinks (name, type, groupid, url) VALUES (?,?,?,?);", [
346✔
24
            $name,
346✔
25
            $type,
346✔
26
            $groupid,
346✔
27
            $url
346✔
28
        ]);
346✔
29

30
        $id = $this->dbhm->lastInsertId();
346✔
31

32
        if ($rc && $id) {
346✔
33
            $this->fetch($this->dbhm, $this->dbhm, $id, 'shortlinks', 'shortlink', $this->publicatts);
346✔
34
            $ret = $id;
346✔
35
        }
36

37
        return($ret);
346✔
38
    }
39

40
    public function resolve($name, $countclicks = TRUE) {
41
        $url = NULL;
3✔
42
        $id = NULL;
3✔
43
        $links = $this->dbhr->preQuery("SELECT * FROM shortlinks WHERE name LIKE ?;", [ $name ]);
3✔
44
        foreach ($links as $link) {
3✔
45
            $id = $link['id'];
3✔
46
            if ($link['type'] == Shortlink::TYPE_GROUP) {
3✔
47
                $g = new Group($this->dbhr, $this->dbhm, $link['groupid']);
2✔
48

49
                # Where we redirect to depends on the group settings.
50
                $external = $g->getPrivate('external');
2✔
51

52
                if ($external) {
2✔
53
                    $url = $external;
×
54
                } else {
55
                    $url = $g->getPrivate('onhere') ? ('https://' . USER_SITE . '/explore/' . $g->getPrivate('nameshort')) : ('https://groups.yahoo.com/' . $g->getPrivate('nameshort'));
2✔
56
                }
57
            } else {
58
                $url = $link['url'];
1✔
59

60
                if (strpos($url, 'http://groups.yahoo.com/group/') === 0) {
1✔
61
                    $url = str_replace('http://groups.yahoo.com/group/', 'http://groups.yahoo.com/neo/groups/', $url);
×
62
                }
63
            }
64

65
            if ($countclicks) {
3✔
66
                $this->dbhm->background("UPDATE shortlinks SET clicks = clicks + 1 WHERE id = {$link['id']};");
2✔
67
                $this->dbhm->background("INSERT INTO shortlink_clicks (shortlinkid) VALUES ({$link['id']});");
2✔
68
            }
69
        }
70

71
        return([$id, $url]);
3✔
72
    }
73

74
    public function listAll($groupid = NULL) {
75
        $groupq = $groupid ? " WHERE groupid = $groupid " : "";
3✔
76
        $links = $this->dbhr->preQuery("SELECT * FROM shortlinks $groupq ORDER BY LOWER(name) ASC;");
3✔
77
        foreach ($links as &$link) {
3✔
78
            if ($link['type'] == Shortlink::TYPE_GROUP) {
3✔
79
                $g = new Group($this->dbhr, $this->dbhm, $link['groupid']);
3✔
80
                $link['nameshort'] = $g->getPrivate('nameshort');
3✔
81

82
                # Where we redirect to depends on the group settings.
83
                $link['url'] = $g->getPrivate('onhere') ? ('https://' . USER_SITE . '/explore/' . $g->getPrivate('nameshort')) : ('https://groups.yahoo.com/neo/groups' . $g->getPrivate('nameshort'));
3✔
84
            }
85
        }
86

87
        return($links);
3✔
88
    }
89

90
    public function getPublic() {
91
        $ret = $this->getAtts($this->publicatts);
3✔
92
        $ret['created'] = Utils::ISODate($ret['created']);
3✔
93

94
        if ($ret['type'] == Shortlink::TYPE_GROUP) {
3✔
95
            $g = new Group($this->dbhr, $this->dbhm, $ret['groupid']);
2✔
96
            $ret['nameshort'] = $g->getPrivate('nameshort');
2✔
97

98
            # Where we redirect to depends on the group settings.
99
            $ret['url'] = $g->getPrivate('onhere') ? ('https://' . USER_SITE . '/explore/' . $g->getPrivate('nameshort')) : ('https://groups.yahoo.com/neo/groups' . $g->getPrivate('nameshort'));
2✔
100
        }
101

102
        $clickhistory = $this->dbhr->preQuery("SELECT DATE(timestamp) AS date, COUNT(*) AS count FROM `shortlink_clicks` WHERE shortlinkid = ? GROUP BY date ORDER BY date ASC", [
3✔
103
            $this->id
3✔
104
        ]);
3✔
105
        foreach ($clickhistory as &$c) {
3✔
106
            $c['date'] = Utils::ISODate($c['date']);
1✔
107
        }
108
        $ret['clickhistory'] = $clickhistory;
3✔
109
        return($ret);
3✔
110
    }
111

112
    public function delete() {
113
        $rc = $this->dbhm->preExec("DELETE FROM shortlinks WHERE id = ?;", [$this->id]);
2✔
114
        return($rc);
2✔
115
    }
116

117
    public function expandExternal($url, $depth = 1) {
118
        $ret = Spam::URL_REMOVED;
41✔
119

120
        if ($depth > 10) {
41✔
121
            # Redirect loop?
122
            error_log("Loop in $url at $depth");
×
123
            return $ret;
×
124
        }
125

126
        if (strpos($url, 'https://' . USER_SITE) === 0 || strpos($url, USER_TEST_SITE) === 0) {
41✔
127
            # Ours - so no need to expand.
128
            error_log("URL $url is our domain " . USER_SITE . " or " . USER_TEST_SITE);
8✔
129
            return $url;
8✔
130
        }
131

132
        if (stripos($url, 'www.facebook.com')) {
39✔
133
            # Facebook bounces us with unsupported browser, but we don't need to expand them anyway.
134
            error_log("URL $url is Facebook");
1✔
135
            return $url;
1✔
136
        }
137

138
        if (stripos($url, 'http') !== 0) {
39✔
139
            # We don't want to follow http links.
140
            error_log("Add http:// to $url");
4✔
141
            $url = "http://$url";
4✔
142
        }
143

144
        try {
145
            # Timeout - if a shortener doesn't return in time we'll filter out the URL.
146
            $opts['http']['timeout'] = 5;
39✔
147
            # Show warnings - e.g. from get_headers.
148
            error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
39✔
149

150
            $context = stream_context_create($opts);
39✔
151

152
            $retries = 10;
39✔
153
            do {
154
                $response = @get_headers($url, 1, $context);
39✔
155

156
                if (!$response) {
39✔
157
                    $retries--;
6✔
158

159
                    if (!$retries) {
6✔
160
                        break;
6✔
161
                    }
162

163
                    usleep(100000);
6✔
164
                }
165
            } while (!$response);
39✔
166

167
            if ($response) {
39✔
168
                # The location property of the response header is used for redirect.
169
                if (array_key_exists('Location', $response)) {
35✔
170
                    $location = $response["Location"];
18✔
171

172
                    if (is_array($location)) {
18✔
173
                        # Find the first entry  in the array starting with http
174
                        $newloc = null;
13✔
175

176
                        foreach ($location as $l) {
13✔
177
                            if (strpos($l, 'http') === 0) {
13✔
178
                                $newloc = $l;
13✔
179
                                break;
13✔
180
                            }
181
                        }
182

183
                        if ($newloc) {
13✔
184
                            $ret = $this->expandExternal($newloc, $depth + 1);
13✔
185
                        } else {
186
                            error_log("Redirect not handled for $url: " . json_encode($location));
×
187
                            $ret = Spam::URL_REMOVED;
13✔
188
                        }
189
                    } else if (stripos($location, 'http') === FALSE) {
18✔
190
                        // Not a link - probably redirecting to relative path.
191
                        $ret = $url;
×
192
                    } else {
193
                        $ret = $this->expandExternal($location, $depth + 1);
18✔
194
                    }
195
                } else {
196
                    $ret = $url;
35✔
197
                }
198
            } else {
199
                error_log("$url returned no response");
39✔
200
            }
201
        } catch (\Exception $e) {
×
202
            error_log("Failed to expand $url: " . $e->getMessage());
×
203
        } finally {
204
            error_reporting(E_ALL & ~E_WARNING & ~E_DEPRECATED & ~E_NOTICE);
39✔
205
        }
206

207
        return $ret;
39✔
208
    }
209

210
    function expandAllUrls($str) {
211
        $urls = [];
287✔
212

213
        if (preg_match_all(Utils::URL_PATTERN, $str, $matches)) {
287✔
214
            foreach ($matches as $val) {
40✔
215
                foreach ($val as $url) {
40✔
216
                    // If url contains ] then use everything before it.  This seems common and wrong, and the regex is
217
                    // impenetrable.
218
                    $url = strstr($url, ']', TRUE) ?: $url;
40✔
219
                    $urls[] = $url;
40✔
220
                }
221
            }
222

223
            $urls = array_unique($urls);
40✔
224

225
            foreach ($urls as $url) {
40✔
226
                if ($url) {
40✔
227
                    $newurl = $this->expandExternal($url);
40✔
228

229
                    if ($newurl != $url) {
40✔
230
                        $str = str_replace($url, $newurl, $str);
21✔
231
                        error_log("Expand $url to $newurl");
21✔
232
                    }
233
                }
234
            }
235
        }
236

237
        return $str;
287✔
238
    }
239
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc