/[drupal]/contributions/modules/links/links.inc
ViewVC logotype

Contents of /contributions/modules/links/links.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.28 - (show annotations) (download) (as text)
Sun Dec 28 00:56:51 2008 UTC (10 months, 4 weeks ago) by syscrusher
Branch: MAIN
CVS Tags: DRUPAL-6--1-0-BETA3, DRUPAL-6--1-0-BETA6, DRUPAL-6--1-0-BETA5, HEAD
Branch point for: DRUPAL-6--1
Changes since 1.27: +108 -6 lines
File MIME type: text/x-php
        Fully functional links_admin.module. Some new
        APIs in links.inc. Removed an extraneous diagnostic
        message from links_weblink.module.

        This is the initial beta release of links_admin.
1 <?php
2 /** @file
3 * $Id: links.inc,v 1.27 2008/12/26 03:53:36 syscrusher Exp $
4 *
5 * links.inc Utility functions for the links modules
6 *
7 * Author: Scott Courtney (Drupal user "syscrusher") scott@4th.com
8 *
9 * NOTE: This file is co-dependent with links.module.
10 */
11
12 /*! @addtogroup links_url URL editing and parsing
13 * @{
14 */
15
16 /**
17 * Certain common GET parameters, such as known session tags (e.g.,
18 * PHPSESSID) don't really belong in a URL that is being permanently
19 * stored. This function strips out these known-undesirable parms.
20 *
21 * WARNING: Changing this function, or others that it calls, may cause
22 * the MD5 sums of URLs in the database to need to be recalculated. If
23 * you make such changes, be sure to make a call to the function
24 * _links_recalc_all_url_md5() in the upgrade script.
25 */
26 function links_normalize_url($url) {
27 // $url = links_remove_session_id($url);
28 // $url = links_remove_empty_get_parms($url);
29 // $url = links_check_trailing_slash($url);
30 // $url = links_sort_get_parms($url);
31 $parsed = links_parse_url($url);
32 $url_n = $parsed['normalized'];
33 return $url_n;
34 }
35
36 /**
37 * Remove empty GET parameters from a URL or bare query string.
38 * For example:
39 * http://example.com/abc.php?this=&that=55&other=
40 * becomes
41 * http://example.com/abc.php?that=55
42 *
43 * TODO: Correctly handle links of the form
44 * http://example.com/abc.xyz?parm=xyz&amp;parm2=123
45 * (character entity encoding of ampersand delimiter)
46 */
47 function links_remove_empty_get_parms($url) {
48 $url = trim($url);
49 // The four consecutive calls are ugly as hell, but the
50 // pregexp misses consecutive empth parms otherwise. For
51 // some reason, substitutions like \1\2 in the replacement
52 // expression don't work as one would expect.
53 $url = preg_replace('/([&?])[^=]+=(&|$)/','\1',$url);
54 $url = preg_replace('/([&?])[^=]+=(&|$)/','\1',$url);
55 $url = preg_replace('/([&?])[^=]+=(&|$)/','\1',$url);
56 $url = preg_replace('/([&?])[^=]+=(&|$)/','\1',$url);
57 // Special handling for empty parm at beginning of string
58 $url = preg_replace('/^([^=]+=)(&.*|$)/','\2',$url);
59 return links_cleanup_parms($url);
60 }
61
62 /**
63 * Order the GET parameters for a URL. This helps to detect
64 * URLs that are the same except for the parameter order.
65 * For exammple:
66 * http://example.com/abc.php?xyz=98&def=yes&abc=49
67 * becomes:
68 * http://example.com/abc.php?abc=49&def=yes&xyz=98
69 *
70 * Compare to links_sort_query_string() (which this function calls).
71 */
72 function links_sort_get_parms($url) {
73 $url = trim($url);
74 $qmark = strpos($url, '?');
75 if ($qmark === FALSE || $qmark == (strlen($url)-1)) {
76 // No GET parms present
77 return $url;
78 }
79 // Intentionally keep the question mark with this part
80 $page = substr($url, 0, $qmark+1);
81 $query = substr($url, $qmark+1);
82 $url = $page . links_sort_query_string($query);
83 return $url;
84 }
85
86 /**
87 * Orders the parameters in an HTTP GET query string. This
88 * helps to detect URLs that are the same except for the
89 * parameter order.
90 * For exammple:
91 * xyz=98&def=yes&abc=49
92 * becomes:
93 * abc=49&def=yes&xyz=98
94 *
95 * Accepts either "&" or the W3C-preferred "&amp;" as a delimiter
96 * between parameters.
97 *
98 * Compare to links_sort_get_parms() (which calls this function).
99 */
100 function links_sort_query_string($query) {
101 // Handles either "&" or "&amp;" as delimiters, or a mixture.
102 $parms = preg_split('/(?:&amp;|&)/i', $query);
103 $p_sort = array();
104 foreach ($parms as $parm) {
105 $eq = strpos($parm, '=');
106 $key = ($eq === FALSE) ? $parm : substr($parm, 0, $eq);
107 $val = $parm;
108 $p_sort[$key] = $val;
109 }
110 ksort($p_sort);
111 // See which delimiter they preferred...believe it or not,
112 // &amp; is actually the W3C preference, but most don't
113 // use it.
114 $delim = strpos($query, '&amp;') === FALSE ? '&' : '&amp;';
115 $query2 = implode($delim, $p_sort);
116 return $query2;
117 }
118
119 /**
120 * This function removes known session ID strings from a URL. These are
121 * transient data from a particular browser on a particular day, and
122 * definitely do not belong in a links database. The search is not
123 * case sensitive. This also works on a bare query string rather than
124 * a full URL, and can be used that way.
125 */
126 function links_remove_session_id($url) {
127 $url = trim($url);
128 $url = preg_replace('/(^|[&?]|&amp;)[0-9A-Z_]*SESS(ION|_ID|ID)*=[0-9A-Z_]*/i','\1',$url);
129 return links_cleanup_parms($url);
130 }
131
132 /**
133 * If a link consists only of a protocol and hostname, add a trailing
134 * slash to explicitly request the default or index document.
135 * For example, "http://www.example.com" becomes "http://www.example.com/".
136 * This slightly speeds up page retrieval for the visitor, and also keeps
137 * from having these non-significant differences reflected in the database.
138 */
139 function links_check_trailing_slash($url) {
140 $url = trim($url);
141 if (preg_match('!^[A-Z0-9]+://[^/?]+$!i', $url)) {
142 $url .= '/';
143 }
144 return $url;
145 }
146
147 /**
148 * The preg_replace() calls in the links_remove_...() functions
149 * may leave some cruft in the URL, such as "&&", "&?", or "?" or "&" as
150 * the first or last character. This function clears that problem up. It is
151 * called by the links_remove_...() functions and may also be
152 * of use to external applications.
153 */
154 function links_cleanup_parms($url) {
155 $patterns = array('/(\&amp;|\&)(\&amp;|\&)/', '/^(\&amp;|\&)/', '/\?(\&amp;|\&)/', '/(\&amp;|\&|\?)$/', '/(\&amp;|\&)\?/');
156 $replaces = array('\1', '', '?', '', '?');
157 $url = preg_replace($patterns, $replaces, $url);
158 return $url;
159 }
160
161 /**
162 * Given a link ID or URL, query the database to suggest a title for this link
163 * as it is being associated with a node. Basically, this will obtain the
164 * first assigned title (ordered by weight) of any existing node associations
165 * for this same link. A default value can be specified in case there's no
166 * good hint in the database. Failing all else, this function will return a
167 * very stripped down version of the URL itself as a suggested title.
168 */
169 function links_suggest_link_title($link_spec, $default="") {
170 // We order the query by weight and then link title, excluding rows where title is empty.
171 $sql = links_get_link_node_query_sql($link_spec, "weight,link_title", FALSE);
172 $result = db_query_range($sql, 1, 1);
173 if (db_error()) {
174 watchdog("error", "links database error on query: $sql");
175 return $default;
176 }
177 $row = db_fetch_array($result);
178 if (is_array($row) && !empty($row["link_title"])) {
179 return $row["link_title"];
180 }
181 // At this point, we didn't find anything really good in the database.
182 // If the caller specified a default, we'll go with that -- but if not,
183 // we still have a few tricks up our sleeve (see below).
184 if (! empty($default)) {
185 return $default;
186 }
187 // Try mangling the URL to make something halfway friendly...
188 $linkrec = links_get_link($link_spec);
189 $url = is_array($linkrec) ? $linkrec["url"] : '';
190 if (empty($url) && ! is_int($link_spec)) {
191 // Sigh...couldn't look it up...but can use the link spec itself
192 $url = $link_spec;
193 }
194 if (! empty($url)) {
195 $url_split = explode('?', links_normalize_url($url));
196 $pre_parms = $url_split[0];
197 $title = empty($url_split[1]) ? $pre_parms : $pre_parms . "?...";
198 // Trim that thing to a reasonable size
199 if (strlen($title) > 80) {
200 $title = substr($title, 0, 75) . '...';
201 }
202 } else {
203 // Method of last resort
204 $title = t("Web Link: ").$link_spec;
205 }
206 return $title;
207 }
208
209 /**
210 * Checks the provided URL for valid syntax, and returns an associative array
211 * to report the findings. The array contains the following elements:
212 *
213 * 'url' => The original URL as passed to the function
214 * 'normalized' => The normalized version of the URL, which is also
215 * validated after normalization. This will be filled
216 * even if the source URL is not valid, though in that
217 * situation its results are undefined.
218 * 'valid' => Boolean TRUE or FALSE to indicate overall status
219 * 'errors' => A sub-array containing one or more error messages
220 * which the caller may optionally provide to the user.
221 * These have NOT been translated with t() before
222 * being returned; that is up to the calling application.
223 * This element will be UNSET if there were no errors.
224 * 'debug' => A sub-array containing diagnostics about the validation
225 * process. Used for testing; each element will be a string,
226 * though these are for programmers and not end users.
227 * 'scheme' => The URL scheme, if present or implied by type (based
228 * on the result URL, not the original), without trailing colon
229 * 'host' => The server name (host and/or domain), also set for
230 * bare email addresses if appropriate options enabled
231 * 'port' => Optional port number, if specified
232 * 'user' => Optional username, if specified, for regular URLS, or
233 * the local part of email addresses
234 * 'pass' => Optional password, if specified
235 * 'path' => An absolute or relative path depending on the URL format
236 * 'query' => GET parameters from the URL as a single string
237 * 'fragment' => Anything after a hashmark (#) character from the
238 * local part of the URL
239 * 'type' => One of the following, depending on the URL category:
240 * 'REMOTE' A traditional URL with sheme and hostname
241 * 'ABSOLUTE' A local absolute path
242 * 'RELATIVE' A local relative (or Drupal) path
243 * 'EMAIL' A bare email address or "mailto:" URL
244 * unset Unknown or invalid URL
245 * 'md5' => The MD5 hash of the lowercased, normalized URL
246 *
247 * The parameters to the function are:
248 *
249 * $url The URL to validate
250 * $accept_bare If TRUE, the function will also accept URLs with no
251 * scheme (that is, without "http://" etc.) as long as
252 * they begin with a valid hostname or match an email
253 * address. In this situation, the returned normalized
254 * URL will add the scheme as appropriate, including
255 * "mailto:" preceding an email address. The default scheme
256 * for anything not beginning with "ftp." is "http", except
257 * that "mailto" will be the scheme for email addresses.
258 * $accept_relative If TRUE, the function will accept relative paths (e.g.,
259 * "node/1234"). In this situation, the returned normalized
260 * URL will not add a scheme or server name. If this is TRUE,
261 * then $accept_absolute is forced TRUE as well.
262 * $accept_absolute If TRUE, the function will accept local absolute paths
263 * (like a Drupal path, only beginning with a slash). in
264 * this situation, the returned normalized URL will not
265 * add a scheme or server name.
266 *
267 * The PHP parse_url() function is deliberately not used here, because its
268 * validation is not extensive since it is intended only as a parser and
269 * not as a validator. The resemblence of this function's return values to
270 * those of parse_url() is not, however, an accident.
271 */
272 function links_parse_url($url, $accept_bare=TRUE, $accept_relative=TRUE, $accept_absolute=TRUE) {
273 $result = array(
274 'url' => $url,
275 'valid' => FALSE,
276 'debug' => array(),
277 );
278 $url_n = trim($url);
279
280 // Trivial rejection
281 if (empty($url_n)) {
282 $result['errors'] = array('Empty URL');
283 return $result;
284 }
285
286 // URI scheme (protocol spec)
287 $p_scheme = "(?:([A-Za-z0-9]+)://|(mailto:))";
288 // Fully qualified domain name (hostname or server name)
289 $p_fqdn = "([a-zA-Z0-9]+\.[a-zA-Z0-9-.]*[a-zA-Z0-9])";
290 // Port specifier
291 $p_port = "(?::([0-9]+))";
292 // Userid or local_part for email address
293 $p_user = "([a-zA-Z0-9_\-.%$]+)";
294 // Password for URLs of the form "scheme://username:password@server/path"
295 $p_passwd = "([a-zA-Z0-9%_~#?&=.,;-]+)";
296 // Combined userid and password authentication spec (see above)
297 $p_auth = "(?:(".$p_user."(?::".$p_passwd.")?)@)";
298 // Combined specification of authentication credentials, hostname, and port
299 $p_server = "(".$p_auth."?".$p_fqdn.$p_port."?)";
300 // Parameters for GET query string
301 // $p_query = "(?:\?([a-zA-Z0-9{}@:%_~&=.,/;-]+))";
302 $p_query = "(?:\?([^#]*))";
303 // Fragment indicator (after "#")
304 $p_frag = "(?:#([a-zA-Z0-9@:%_?~&=.,/;-]*))";
305 // The path catches the leading slash, if present, separately
306 // so that we can later distinguish between relative and absolute
307 // $p_path = "(/?)([a-zA-Z0-9@:%_~&=.,/;-]*)";
308 $p_path = "(/?)([^?#]*)";
309 $p_local = $p_path . $p_query . "?" . $p_frag . "?";
310
311 // Set some path components based on optional parameters
312 if ($accept_bare) {
313 $p_scheme .= "?";
314 }
315 $p_inet = "(".$p_scheme.$p_server.")";
316 if ($accept_relative) {
317 $accept_absolute = TRUE;
318 }
319 if ($accept_absolute) {
320 // The entire remote host spec is optional
321 $p_inet .= "?";
322 }
323
324 // Now try to find a match
325 $pattern = "!".$p_inet.$p_local."$!i";
326 $result['debug'][] = "Matching for regular URL using pattern: ".$pattern;
327 $matches = array();
328 preg_match($pattern, $url_n, $matches);
329 if (count($matches) > 0) {
330 if ($matches[3] == "mailto:"
331 || (empty($matches[2]) && empty($matches[3])
332 && !empty($matches[6]) && !empty($matches[8])
333 && empty($matches[10]) && empty($matches[11]))) {
334 $result['scheme'] = "mailto";
335 $result['type'] = 'EMAIL';
336 }
337 if (! isset($result['type'])) {
338 $result['scheme'] = $matches[2];
339 if (! empty($result['scheme'])) {
340 $result['type'] = 'REMOTE';
341 $result['path'] = "/" . $matches[11];
342 $result['port'] = $matches[9];
343 } else if (! empty($matches[8])) {
344 if (preg_match("!^ftp.!", $matches[8])) {
345 $result['debug'][] = "Assuming remote FTP protocol for bare URL based on host name";
346 $result['scheme'] = 'ftp';
347 } else {
348 $result['debug'][] = "Assuming remote HTTP protocol for bare URL";
349 $result['scheme'] = 'http';
350 }
351 $result['type'] = 'REMOTE';
352 $result['path'] = "/" . $matches[11];
353 $result['port'] = $matches[9];
354 } else {
355 if (!empty($matches[11])) {
356 if (empty($matches[10])) {
357 // Local relative path
358 $result['type'] = 'RELATIVE';
359 $result['path'] = $matches[11];
360 } else {
361 // Local absolute path
362 $result['type'] = 'ABSOLUTE';
363 $result['path'] = '/' . $matches[11];
364 }
365 }
366 }
367 if (isset($result['type'])) {
368 $result['pass'] = $matches[7];
369 $result['query'] = $matches[12];
370 $result['fragment'] = $matches[13];
371 }
372 }
373 if (isset($result['type'])) {
374 $result['host'] = $matches[8];
375 $result['user'] = $matches[6];
376 }
377 }
378 if (isset($result['type'])) {
379 $result['valid'] = TRUE;
380 // Build the normalized URL
381 $norm = '';
382 switch ($result['scheme']) {
383 case 'mailto':
384 $norm .= 'mailto:';
385 break;
386 case '':
387 break;
388 default:
389 $norm .= $result['scheme'] . '://';
390 }
391 $type = $result['type'];
392 if (! empty($result['user'])) {
393 $norm .= $result['user'];
394 if (! empty($result['pass'])) {
395 $norm .= ':' . $result['pass'];
396 }
397 $norm .= '@';
398 }
399 $norm .= $result['host'];
400 if (! empty($result['port'])) {
401 $norm .= ':' . $result['port'];
402 }
403 $norm .= $result['path'];
404 if (! empty($result['query'])) {
405 $result['debug'][] = "Raw query as extracted: ".$result['query'];
406 $result['query'] = links_remove_session_id($result['query']);
407 $result['debug'][] = "Query after removing session IDs: ".$result['query'];
408 $result['query'] = links_remove_empty_get_parms($result['query']);
409 $result['debug'][] = "Query after removing empty parms: ".$result['query'];
410 $result['query'] = links_sort_query_string($result['query']);
411 $result['debug'][] = "Sorted query string: ".$result['query'];
412 }
413 if (! empty($result['query'])) {
414 $norm .= '?' . $result['query'];
415 }
416 if (! empty($result['fragment'])) {
417 $norm .= '#' . $result['fragment'];
418 }
419 $result['normalized'] = $norm;
420 $result['md5'] = md5(strtolower($norm));
421 }
422 return $result;
423 }
424
425 /*!
426 * @}
427 */
428
429 /*! @addtogroup links_text Text search and edit functions
430 * @{
431 */
432
433 /**
434 * Search a text string, potentially multiline, and return an array
435 * containing all of the URLs found in that string. The array is
436 * an integer-subscripted array with the subscript reflecting the
437 * order in which the links were found.
438 *
439 * Each element of the main array is an inner array per link. These are
440 * associative, with the keys being:
441 * "matched" => the actual text matched in the search, which is
442 * later to be replaced with a "goto" link.
443 * "url" => the normalized URL to which this link should go;
444 * it may not be identical to the URL in the text.
445 * "http://" is added to bare URLs that don't specify
446 * a protocol.
447 * "title" => a nominal title string for the link. For links that
448 * had an HTML <A> tag around them, this is the text
449 * inside the tag. For bare links, this is the same
450 * as the url originally found in the text (the assumption
451 * being that this is what the author wanted to present).
452 *
453 * Some of the pregexps are adapted from the weblinks module, and others
454 * are adapted from examples on php.net. In all cases, they are complex
455 * and should be changed with great care and tested thoroughly, as small
456 * changes here could have large and unintended consequences!
457 */
458 function links_find_links($text) {
459 $links = array();
460 $text = ' ' . $text . ' ';
461 $patterns = array();
462 // Finds links with a protocol spec (adapted from urlfilter.module)
463 $patterns['bare_url'] = "!(?:<p>|<br>|<br />|[ \n\r\t\(])((?:(?:[A-Za-z0-9]+://)|mailto:)(?:[a-zA-Z0-9@:%_~#?&=.,/;-]*[a-zA-Z0-9@:%_~#&=/;-]))([.,?]?)(?=(</p>|[ \n\r\t\)]))!i";
464 // Finds WWW or FTP links without a protocol spec (adapted from urlfilter.module)
465 $patterns['bare_no_protocol'] = "!(?:<p>|<br>|<br />|[ \n\r\t\(])((?:www|ftp)\.[a-zA-Z0-9@:%_~#?&=.,/;-]*[a-zA-Z0-9@:%_~#\&=/;-])(?:[.,?]?)(?=(</p>|[ \n\r\t\)]))!i";
466 // Finds pre-tagged links (pattern adapted from one posted to php.net by "martin at vertikal dot dk")
467 $patterns['tag'] = "!<a href=(?:\"([^\">]+)\"[^>]*|([^\" >]+?)[^>]*)>(.*)</a>!Uis";
468 reset($patterns);
469 while (list($type, $pattern) = each($patterns)) {
470 $found = array();
471 $count = preg_match_all($pattern, $text, $found);
472 $howmany = count($found[0]);
473 switch ($type) {
474 case 'bare_url':
475 $found[0] = $found[1];
476 $found[2] = $found[1];
477 unset($found[3]);
478 break;
479 case 'bare_no_protocol':
480 $found[0] = $found[1];
481 $found[2] = $found[1];
482 for ($i=0; $i<$howmany; $i++) {
483 $found[1][$i] = "http://" . $found[1][$i];
484 }
485 break;
486 case 'tag':
487 // $found[1] = $found[2];
488 $found[2] = $found[3];
489 break;
490 }
491 // At this point, $found is a nested array where outer subscripts
492 // are as follows:
493 // 0 => array of all the text matched in the original item
494 // 1 => array of all the physical URLs that should be linked
495 // 2 => array of nominal display text for the links
496 for ($i=0; $i<$howmany; $i++) {
497 // Create an associative array for one link
498 $matched = $found[0][$i];
499 $url = links_normalize_url($found[1][$i]);
500 $title = $found[2][$i];
501 // We may have line breaks in the title -- remove them
502 $title = preg_replace("![\n\r\t]!",' ',$title,-1);
503 $link = array("matched"=>$matched, "url"=>$url, "title"=>$title);
504 $links[] = $link;
505 }
506 }
507 return $links;
508 }
509
510 /*!
511 * @}
512 */
513
514 // ********* FUNCTIONS FOR MANAGING THE URL HASH ***************
515
516 /*!
517 * @addtogroup links_hash URL hash management
518 * @{
519 */
520
521 /**
522 * Given a URL, this function returns an MD5 hash of a case-insensitive,
523 * normalized version of that URL. The idea of this function is that two
524 * URLs that differ only in ways that are not significant to a web
525 * browser will have the same MD5 in the database. See the warning for
526 * function links_normalize_url() for information concerning
527 * changes made to this code in new versions.
528 */
529 function links_hash_url($url) {
530 $url = strtolower(links_normalize_url($url));
531 return md5($url);
532 }
533
534 /*!
535 * @}
536 */
537
538 // ************** FUNCTIONS FOR DATABASE QUERIES AND UPDATES ****************
539
540 /*!
541 * @addtogroup links_db Link storage and retrieval
542 * @{
543 */
544
545 /**
546 * This internal function creates an SQL WHERE clause to select a
547 * record from the {links} table based on an integer link ID
548 * or a URL (which is normalized and hashed before the query). The
549 * returned SQL will be a WHERE clause with leading and trailing blanks
550 * for convenient concatenation. Note that no query is executed here.
551 *
552 * Accepts an MD5 string for the $link_spec, to match directly against
553 * the url_md5 field in the database. The assumption is that a raw
554 * hex number exactly 32 characters in length won't be a URL. The hex
555 * characters A-F are matched case-insensitively.
556 *
557 * The $tablename optional parameter defaults to {links} but can
558 * specify a table alias if this WHERE is to be used in a join query.
559 * Remember to use the curly-brackets for actual table names, but not
560 * for alias names, so that database prefixes will work right.
561 */
562 function links_get_link_where_sql($link_spec, $tablename="{links}") {
563 $where = " WHERE ".$tablename;
564 // @TODO fix this - it just fails if the hash happens to start with a numeric (DOH)
565 if (intval($link_spec)) {
566 $where = $where . ".lid=" . $link_spec . " ";
567 } else {
568 if (preg_match('/^[0-9a-f]{32}$/i', $link_spec)) {
569 $hash = $link_spec;
570 } else {
571 $hash = links_hash_url($link_spec);
572 }
573 $where = $where . ".url_md5='" . $hash . "' ";
574 }
575 return $where;
576 }
577
578 /**
579 * Given a URL, URL hash, or link ID (lid), retrieves any existing link record
580 * from the database as an array.
581 *
582 * @param string $link_spec The URL or hash of the URL to be fetched
583 */
584 function links_get_link($link_spec) {
585 $where = links_get_link_where_sql($link_spec);
586 $sql = "SELECT * FROM {links}" . $where;
587 $result = db_query($sql);
588 if (db_error()) {
589 watchdog("links","links could not retrieve link matching '".$link_spec."'", array(), WATCHDOG_ERROR);
590 return NULL;
591 }
592 if ($link = db_fetch_array($result)){
593 $link['link_title'] = trim($link['link_title']);
594 $link['url'] = trim($link['url']);
595 }
596 return $link;
597 }
598
599 /**
600 * This function deletes a link record specified by its lid.
601 * Before doing this, it invokes the hook 'links_delete_link_references'
602 * with the specified link ID so that other modules can do what they
603 * need to do in order to adjust. In some cases, this may actually
604 * mean the other modules need to delete nodes.
605 */
606 function links_delete_link($lid) {
607 module_invoke_all('links_delete_link_reference', $lid);
608 // This query exists in case a module implementor failed to
609 // implement the hook. It makes sure the table is tidy,
610 // but provides no opportunity for the module to add
611 // customized behavior.
612 $sql = "DELETE FROM {links_node} WHERE lid=%d";
613 db_query($sql, $lid);
614 if (db_error()) {
615 watchdog("links","Links could not delete link !lid from {links_node} table", array('!lid'=>$lid), WATCHDOG_ERROR);
616 // Treat this one as non-fatal, so no return here
617 } else {
618 watchdog("links","Executed backstop clearing of {links_node} for lid=!lid", array('!lid'=>$lid), WATCHDOG_INFO);
619 }
620 // Now delete the actual link.
621 $sql = "DELETE FROM {links} WHERE lid=%d";
622 db_query($sql, $lid);
623 if (db_error()) {
624 watchdog("links", "Failed to delete link !lid", array('!lid'=>$lid), WATCHDOG_ERROR);
625 return FALSE;
626 } else {
627 watchdog("links", "Deleted link !lid", array('!lid'=>$lid), WATCHDOG_INFO);
628 return TRUE;
629 }
630 }
631
632 /**
633 * This low-level function updates an existing link record in place. If the
634 * URL has changed, it checks for another existing record that already matches
635 * the *new* URL, and returns that link ID if found. Otherwise, updates the
636 * URL and/or title of the existing record.
637 *
638 * If the existing record is not found, an insert will be attempted.
639 *
640 * In any case, if things are successful, the *new* $lid (which may or
641 * may not be the same as the old one) is returned. The caller should
642 * then call module_invoke_all('links_update_link_reference', $old_lid, $new_lid)
643 * to let other modules update any referring tables if needed.
644 *
645 * @see links_update_link
646 */
647 function links_update_link_basic($old_lid, $url, $title='') {
648 if ($old_lid) {
649 $old = links_get_link($old_lid);
650 } else {
651 $old = links_get_link($url);
652 }
653 if (! is_array($old)) {
654 // No extant record. Attempt insert.
655 return links_put_link($url, $title);
656 } else {
657 $title = check_plain(trim($title));
658 $old_lid = $old['lid'];
659 $new_hash = links_hash_url($url);
660 if ($new_hash == $old['url_md5']) {
661 if ($title == $old['link_title']) {
662 // Nothing to do!
663 return $old_lid;
664 }
665 // Safe to update the title in place
666 $sql = "UPDATE {links} SET link_title='%s' WHERE lid=%d";
667 $result = db_query($sql, $title, $old_lid);
668 if (db_error()) {
669 drupal_set_message(t('Update of title in existing link record failed.'), 'error');
670 watchdog("links","links update for existing link !lid failed", array('!lid'=>$old_lid), WATCHDOG_ERROR);
671 return 0;
672 }
673 return $old_lid;
674 } else {
675 // The URL has changed
676 $extant = links_get_link($url);
677 if (is_array($extant) && $extant['lid'] != $old_lid) {
678 // An attempt was made to change this link's URL to match one already in the
679 // database. What we need to do, therefore, is to return the existing record's
680 // key. Higher-level code should update references and then delete the old
681 // record ($old_lid) and keep the new one ($extant['lid']) instead.
682 return $extant['lid'];
683 } else {
684 // It's actually safe to update the URL and the title in place
685 $sql = "UPDATE {links} SET link_title='%s', url='%s', url_md5='%s' WHERE lid=%d";
686 $title = trim($title);
687 $result = db_query($sql, $title, $url, $new_hash, $old_lid);
688 if (db_error()) {
689 drupal_set_message(t('Update of URL and title in existing link record failed.'), 'error');
690 watchdog("links","Links update for existing link !lid failed", array('!lid'=>$old_lid), WATCHDOG_ERROR);
691 return 0;
692 } else {
693 watchdog("links","Changed URL for existing link !lid to !url", array('!lid'=>$old_lid, '!url'=>$url), WATCHDOG_INFO);
694 }
695 return $old_lid;
696 }
697 }
698 }
699 }
700
701 /**
702 * For a specified link ID, checks all existing node references to be sure
703 * the title is really needed in the local record. If appropriate, the
704 * {links_node} record is set to use the master record's title. This is
705 * typically invoked after changing the title in the master record, because
706 * one or more of the node records may no longer need its title.
707 */
708 function links_check_node_titles($lid) {
709 $master = links_get_link($lid);
710 if (! $master['lid']) {
711 // Don't do anything if no master record.
712 return;
713 }
714 $result = db_query("SELECT * FROM {links_node} WHERE lid=%d", $lid);
715 while ($row = db_fetch_array($result)) {
716 // In most cases this become a no-op
717 links_set_local_title($lid, $row['nid'], $row['module'], $row['link_title']);
718 }
719 }
720
721 /**
722 * This is a higher-level wrapper around links_update_link_basic() that is
723 * well-behaved with respect to updating references to tables. Anything in
724 * {links_node} will be taken care of by our own code. Other modules only
725 * need to implement the hook if they use custom tables.
726 */
727 function links_update_link($old_lid, $url, $title='') {
728 $new_lid = links_update_link_basic($old_lid, $url, $title);
729 links_check_node_titles($new_lid);
730 if ($new_lid && $old_lid && ($new_lid != $old_lid)) {
731 watchdog("links", "Updated link ID from !old to !new. Calling hook to update references, then old link will be deleted.", array('!old'=>$old_lid, '!new'=>$new_lid), WATCHDOG_INFO);
732 module_invoke_all('links_update_link_reference', $old_lid, $new_lid);
733 // Now delete the old link that is no longer used anywhere
734 links_delete_link($old_lid);
735 }
736 }
737
738 /**
739 * Forces all of the references in {links_node} for the
740 * specified $lid to use the master title rather than their
741 * node-local title, if any.
742 *
743 * $lid is required; the other two parameters are optional.
744 */
745 function links_force_master_title($lid, $nid=0, $module='') {
746 if ($nid==0) {
747 if (empty($module)) {
748 db_query("UPDATE {links_node} SET link_title='' WHERE lid=%d", $lid);
749 } else {
750 db_query("UPDATE {links_node} SET link_title='' WHERE lid=%d AND module='%s'", $lid, $module);
751 }
752 } else {
753 if (empty($module)) {
754 db_query("UPDATE {links_node} SET link_title='' WHERE lid=%d AND nid=%d", $lid, $nid);
755 } else {
756 db_query("UPDATE {links_node} SET link_title='' WHERE lid=%d AND nid=%d AND module='%s'", $lid, $nid, $module);
757 }
758 }
759 if (db_error()) {
760 watchdog("links", "Failed to force master title for link !lid", array('!lid'=>$lid), WATCHDOG_ERROR);
761 } else {
762 watchdog("links", "Forcing master title for link !lid", array('!lid'=>$lid), WATCHDOG_INFO);
763 }
764 }
765
766 /**
767 * Returns the count of how many references exist for a given link in
768 * {links_node}. If $module is specified, the query is limited to that
769 * module only.
770 *
771 * TODO: Add more robust error handling.
772 */
773 function links_count_nodes($link_spec, $module=NULL) {
774 $link = links_get_link($link_spec);
775 if (! is_array($link)) {
776 return 0;
777 }
778 $lid = intval($link['lid']);
779 if (empty($module)) {
780 $result = db_query("SELECT COUNT(*) AS n FROM {links_node} WHERE lid=%d", $lid);
781 } else {
782 $result = db_query("SELECT COUNT(*) AS n FROM {links_node} WHERE lid=%d AND module='%s'", $lid, $module);
783 }
784 $row = db_fetch_array($result);
785 if (is_array($row)) {
786 return intval($row['n']);
787 } else {
788 return 0;
789 }
790 }
791
792 /**
793 * Implements hook_links_update_link_reference to change $old_lid to
794 * $new_lid in our own tables. Here we update only {links_node}.
795 */
796 function links_links_update_link_reference($old_lid, $new_lid) {
797 db_query("UPDATE {links_node} SET lid=%d WHERE lid=%d", $new_lid, $old_lid);
798 if (db_error()) {
799 watchdog("links","links update for existing link record failed", array(), WATCHDOG_ERROR);
800 }
801 }
802
803 /*
804 * Gets a specific record from {links_node}
805 */
806 function links_get_node_reference($lid, $nid, $module) {
807 $result = db_query("SELECT * FROM {links_node} WHERE lid=%d AND nid=%d AND module='%s'", $lid, $nid, $module);
808 if (db_error()) {
809 watchdog("links", "database error while retrieving link reference lid=!lid, nid=!nid, module=!module", array('!lid'=>$lid, '!nid'=>$nid, '!module'=>$module), WATCHDOG_ERROR);
810 return NULL;
811 }
812 if ($row = db_fetch_array($result)) {
813 return $row;
814 } else {
815 return array();
816 }
817 }
818
819 /*
820 * Sets the local title for a specified link reference. If the affected
821 * node is the ONLY node using the link, this will also update the master
822 * link if the user has access to do so. Note that this function presumes
823 * a pre-existing {links_node} record.
824 */
825 function links_set_local_title($lid, $nid, $module, $new_title) {
826 $link = links_get_link($lid);
827 if (! is_array($link)) {
828 watchdog("links", "cannot find master link for !lid while updating title in node reference", array('!lid'=>$lid), WATCHDOG_ERROR);
829 return;
830 }
831 $master_title = $link['link_title'];
832 $new_title = check_plain(trim($new_title));
833 if (empty($new_title) || $new_title == $master_title) {
834 // An empty local title, or one matching the master title, says to use the master title
835 links_force_master_title($lid, $nid, $module);
836 return;
837 }
838 // Get the existing local title
839 $old = links_get_node_reference($lid, $nid, $module);
840 $old_title = $old['link_title'];
841
842 // If the new and old titles are equal, nothing to do. Otherwise, update
843 // and set the master title if this is the only referring node.
844 if ($new_title != $old_title) {
845 $refcount = links_count_nodes($lid);
846 if ($refcount > 1 || ! user_access('administer links')) {
847 // Conventional local update
848 db_query("UPDATE {links_node} SET link_title='%s' WHERE lid=%d AND nid=%d AND module='%s'", $new_title, $lid, $nid, $module);
849 } else {
850 // Update the master record
851 db_query("UPDATE {links} SET link_title='%s' WHERE lid=%d", $new_title, $lid);
852 if (! db_error()) {
853 db_query("UPDATE {links_node} SET link_title='' WHERE lid=%d AND nid=%d AND module='%s'", $lid, $nid, $module);
854 }
855 }
856 if (db_error()) {
857 watchdog("links", "links update for link !lid, node !nid failed", array('!lid'=>$lid, '!nid'=>$nid), WATCHDOG_ERROR);
858 }
859 }
860 }
861
862 /**
863 * Implements hook_links_delete_link_reference to remove $lid from
864 * the {links_node} table for cases where the 'module' colum is
865 * empty or null (which it shouldn't be, but...). Any module that
866 * uses {links_node} should implement this hook as well.
867 */
868 function links_links_delete_link_reference($lid) {
869 // Here we just delete anything for null or empty module,
870 // which at least in theory shouldn't happen. If you implement
871 // this hook in your module, put your module's name in the
872 // appropriate field as a constant.
873 db_query("DELETE FROM {links_node} WHERE lid=%d AND module=''", $lid);
874 if (db_error()) {
875 watchdog("links","links delete references for link !lid failed for null module", array('!lid'=>$lid), WATCHDOG_ERROR);
876 } else {
877 watchdog("links","Deleted references for link !lid for null module", array('!lid'=>$lid), WATCHDOG_INFO);
878 }
879 }
880
881 /**
882 * This low-level function simply adds a bare URL record, with no associated
883 * node, to the database. It returns the new link ID number
884 * if successful, or 0 if it fails.
885 *
886 * The function checks for an existing matching link before adding a new
887 * record, and if one is found, that links ID is returned instead of a new
888 * one being assigned.
889 *
890 * The optional title field is only used if a new link is being inserted.
891 */
892 function links_put_link($url, $title='') {
893 $hash = links_hash_url($url);
894 $sql = "SELECT * FROM {links} WHERE url_md5='%s'";
895 $result = db_query($sql,$hash);
896 if (db_error()) {
897 // Log an error message and return failure
898 drupal_set_message(t('Query for existing link record failed.'), 'error');
899 watchdog("links","links query for existing link record failed", array(), WATCHDOG_ERROR);
900 return 0;
901 }
902 if ($result) {
903 $row = db_fetch_array($result);
904 if ($row["lid"]) {
905 // Existing record found
906 return $row["lid"];
907 }
908 }
909 // Need to create a new record
910 $url = links_normalize_url($url);
911 if (empty($title)) {
912 $title = links_suggest_link_title($url);
913 }
914 $sql = "INSERT INTO {links} (lid, url, url_md5, link_title) VALUES (%d, '%s', '%s', '%s')";
915 $result = db_query($sql, $lid, $url, $hash, $title);
916 $lid = db_last_insert_id('{links}','lid');
917 if (db_error()) {
918 // Failed to add record -- log and fail
919 watchdog("error","Unable to insert URL '".$url."' into links as ID $lid");
920 return 0;
921 }
922 // Success!
923 return $lid;
924 }
925
926 /*!
927 * @}
928 *
929 * @addtogroup links_node Link-Node associations
930 * @{
931 */
932
933 /**
934 * Given a link ID, URL, or URL hash, creates an SQL query to obtain brief node data for
935 * nodes that share that link's record. Columns returned from this query include:
936 * lid The link ID number (integer)
937 * url The actual (full) URL from the link record
938 * nid The node ID number (integer)
939 * node_title The node title (string)
940 * weight The weight of this link-node pairing, which determines the
941 * order when links are listed together.
942 * clicks The number of times a browser has followed this particular
943 * instance of this particular link-node association.
944 * link_title The title given to the link (string); this is the text that
945 * is displayed for the user as the title of the link. How and
946 * where it appears (on the page, mouse over, not at all...)
947 * depends on the settings of user and admin preferences.
948 * module If a specific module wants to own the link-node association,
949 * insert that module's name here. The default is 'links', which
950 * represents the entire links package collectively.
951 *
952 * Note that lid and url fields are the same on all returned rows; they are
953 * here for convenience.
954 *
955 * The optional parameter $order determines whether the outer array is
956 * ordered by the link's weight, by link_title, or by node_title. If
957 * $order is an empty string (not the default!), no ORDER BY will be added; this
958 * allows the caller more control, such as appending a WHERE clause or doing
959 * a compound ordering on multiple columns. The default orders by weight and
960 * then by link_title.
961 *
962 * Left joins are intentionally used in this query, with the deliberate possibility
963 * to return rows with NULL values in cases of (1) a link with no associated nodes,
964 * or (2) a link associated with node IDs that no longer exist. This is useful
965 * to find orphan records. To suppress this feature, set the optional parameter
966 * $orphans to FALSE (it defaults to TRUE).
967 *
968 * Note that this function returns an SQL statement but does not actually
969 * execute the query.
970 */
971 function links_get_link_node_query_sql($link_spec, $order="weight,link_title", $orphans=TRUE, $module='links') {
972 $sql = "SELECT l.lid, l.url, ln.nid, n.title AS node_title, ln.weight, ln.clicks, ln.link_title AS link_title ";
973 $sql .= "FROM {links} AS l LEFT JOIN {links_node} AS ln ON l.lid=ln.nid ";
974 $sql .= "LEFT JOIN {node} AS n ON ln.nid=n.nid ";
975 $sql .= links_get_link_where_sql($link_spec, "l");
976 if (! $orphans) {
977 $sql .= " AND ln.nid IS NOT NULL AND n.nid IS NOT NULL ";
978 }
979 if (! empty($module)) {
980 $sql .= " AND ln.module='" . $module . "' ";
981 }
982 switch ($order) {
983 case "":
984 break;
985 case "node_title":
986 case "link_title":
987 $sql .= " ORDER BY " . $order;
988 break;
989 default:
990 $sql .= " ORDER BY weight, link_title";
991 }
992 $sql .= " ";
993 return $sql;
994 }
995
996 /**
997 * Delete all the links for the specified node (object)
998 * or nid (integer)
999 */
1000 function links_delete_links_for_node($node, $module='links_links') {
1001 $sql = "DELETE FROM {links_node} WHERE nid=%d";
1002 if (! empty($module)) {
1003 $sql .= " AND module='" . $module . "' ";
1004 }
1005 if (is_object($node)) {
1006 return db_query($sql, $node->nid);
1007 } else {
1008 return db_query($sql, $node);
1009 }
1010 }
1011
1012 /**
1013 * Save all the links for the specified node (object). The links to be
1014 * saved are assumed to exist as sub-arrays inside the main integer-
1015 * subscripted array named $node->$module, where $module is the
1016 * parameter that specifies the context for these links.
1017 *
1018 * Each link is an associative sub-array containing the fields 'url',
1019 * 'weight', and 'link_title'. The 'url' field is self-explanatory.
1020 * 'weight' will determine, when multiple links exist, their display
1021 * order when the node is rendered. 'link_title' is stored as either
1022 * the node-local title for the link, or as the global link title if
1023 * the link is new to the catalog. 'url' is the only required field;
1024 * the others have defaults.
1025 *
1026 * There is no equivalent function to save a single link for a node. If
1027 * modules require that functionality, they should simply pass an array
1028 * with only one link sub-array.
1029 *
1030 * @param &$node The node for which links are being saved
1031 * @param $module The module controlling the link(s)
1032 * @param $global If TRUE, modification of a URL for any of these
1033 * links will change that link's URL globally. If
1034 * FALSE (the default), a changed URL results in a
1035 * separate catalog entry, leaving other nodes' URLs
1036 * untouched. $global is ignored if the current user
1037 * lacks the 'change url globally' permission (set in
1038 * links.module).
1039 */
1040 function links_save_links_for_node(&$node, $module='links_links', $global=FALSE) {
1041 $oldlinks =& links_load_links_for_node($node->nid, $module, NULL, NULL, 'lid');
1042 $links =& $node->$module;
1043 $all_ok = TRUE;
1044 // This will track the link IDs that should be kept
1045 $lids = array();
1046 foreach ($links as $i => $link) {
1047 $url = links_normalize_url($link['url']);
1048 $link['url'] = $url;
1049 $title = trim($link['link_title']);
1050 $link['link_title'] = $title;
1051 if (empty($url) || $link['delete']) {
1052 // Ignore empty URL, either from unused form fields or from
1053 // blanked-out existing URL field. In the latter case, this
1054 // link will be removed from the node later in this function.
1055 // Same action if the "delete" flag (boolean) is set in the
1056 // array (this normally would happen from a UI form).
1057 continue;
1058 }
1059 // Make sure the base link record exists, one way
1060 // or another.
1061 $lid = links_put_link($url, $link['link_title']);
1062 // $base_rec gets the full record from {links} whether
1063 // it was inserted or just retrieved
1064 $base_rec = links_get_link($lid);
1065 if (isset($oldlinks[$lid])) {
1066 // Old record from this node
1067 $old_rec = $oldlinks[$lid];
1068 } else {
1069 // Defaults for some fields
1070 $old_rec = $base_rec;
1071 }
1072 if (is_array($base_rec) && $base_rec['lid']) {
1073 $link['lid'] = $old_rec['lid'];
1074 $lids[] = $old_rec['lid'];
1075 $new_title = trim($link['link_title']);
1076 $master_title = trim($base_rec['link_title']);
1077 if (empty($new_title)) {
1078 $suggested = TRUE;
1079 $new_title = links_suggest_link_title($base_rec['lid'], '');
1080 } else {
1081 $suggested = FALSE;
1082 }
1083 // If we are first to title an existing link, make that
1084 // update directly before anything else
1085 if (empty($master_title)) {
1086 $sql = "UPDATE {links} SET link_title='%s' WHERE lid=%d";
1087 $result = db_query($sql, $new_title, $lid);
1088 $master_title = $new_title;
1089 }
1090 // Now do the rest
1091 if ($new_title == $master_title || $suggested) {
1092 // Leave the node-specific title empty if we
1093 // already have it in the master record. Also,
1094 // an API-suggested title goes *only* in the master
1095 // record, and if applicable, we did that above.
1096 $new_title = '';
1097 } else {
1098 if (empty($new_title)) {
1099 $new_title = $master_title;
1100 }
1101 }
1102 } else {
1103 drupal_set_message(t('Cannot find or create link record for %url',array('%url'=>$url)),'error');
1104 $all_ok = FALSE;
1105 }
1106 $weight = intval($link['weight']);
1107 if (isset($oldlinks[$lid])) {
1108 $sql = "UPDATE {links_node} SET link_title='%s', weight=%d WHERE lid=%d AND nid=%d AND module='%s'";
1109 $op = t('update');
1110 } else {
1111 $sql = "INSERT INTO {links_node} (link_title, weight, lid, nid, clicks, module) VALUES ('%s',%d,%d,%d,0,'%s')";
1112 $op = t('insert');
1113 }
1114 $result = db_query($sql, $new_title, $weight, $lid, $node->nid, $module);
1115 if (! $result) {
1116 drupal_set_message(t('Unable to %op link &quot;%t&quot; in database.', array('%op'=>$op, '%t'=>$link['link_title'])),'error');
1117 $all_ok = FALSE;
1118 }
1119 }
1120 // Now delete any leftover links from before, that weren't in the group we
1121 // just confirmed should stay
1122 if (count($lids)) {
1123 $inlist = implode(',',$lids);
1124 $sql = "DELETE FROM {links_node} WHERE nid=%d AND lid NOT IN (%s) AND module='%s'";
1125 db_query($sql,$node->nid,$inlist, $module);
1126 } else {
1127 // There now are NO links for this node
1128 links_delete_links_for_node($node, $module);
1129 }
1130 return $all_ok;
1131 }
1132
1133 /**
1134 * Given a node ID, returns all of the links currently defined for that node in
1135 * the database. The return value is a compound array, with the outer subscript
1136 * being an integer index and the elements associative arrays with the fields
1137 * lid, url, url_md5, title, clicks, and weight.
1138 *
1139 * For each record returned, there are actually up to *three* title fields:
1140 * link_link_title the title from the main {links} record
1141 * node_link_title the title, if any, from the {links_node} record
1142 * link_title the node_link_title if present, or the link_link_title
1143 * otherwise. This is normally the field that applications
1144 * will want to use for display.
1145 *
1146 * If a specific link ID is provided, returns ONLY that record. If $firstonly
1147 * is TRUE, returns only one record. $firstonly is provided as a convenience for
1148 * handling a "goto" URL pattern from the browser that specifies nid but not lid.
1149 * This is used, among other things, for legacy compatibility with weblink.module.
1150 */
1151 function links_load_links_for_node($nid, $module='links', $lid=0, $firstonly=FALSE, $indexedby = NULL) {
1152 $links = array();
1153 if ($lid) {
1154 $sql = "SELECT l.lid, ln.nid, url, url_md5, weight, clicks, module, l.link_title AS link_link_title, ln.link_title AS node_link_title FROM {links_node} ln LEFT JOIN {links} l ON l.lid=ln.lid WHERE ln.nid=%d AND l.lid=%d";
1155 if (! empty($module)) {
1156 $sql .= " AND module='" . $module . "' ";
1157 }
1158 $sql .= " ORDER BY weight, l.link_title";
1159 $result = db_query($sql, array($nid, $lid));
1160 } else {
1161 $sql = "SELECT l.lid, ln.nid, url, url_md5, weight, clicks, module, l.link_title AS link_link_title, ln.link_title AS node_link_title FROM {links_node} ln LEFT JOIN {links} l ON l.lid=ln.lid WHERE ln.nid=%d";
1162 if (! empty($module)) {
1163 $sql .= " AND module='" . $module . "' ";
1164 }
1165 $sql .= " ORDER BY weight, l.link_title";
1166 $result = db_query($sql, array($nid));
1167 }
1168 if (db_error($result)) {
1169 watchdog("error",t("links failed on query ").$sql);
1170 } else {
1171 while ($row = db_fetch_array($result)) {
1172 $row['link_title'] = trim($row['link_title']);
1173 $row['link_title'] = trim(empty($row['node_link_title']) ? $row['link_link_title'] : $row['node_link_title']);
1174 if($indexedby){
1175 $links[$row[$indexedby]] = $row;
1176 }
1177 else {
1178 $links[] = $row;
1179 }
1180 if ($firstonly) {
1181 break;
1182 }
1183 }
1184 }
1185 return $links;
1186 }
1187
1188 /**
1189 * Given a node ID and an optional link ID, goes to either the specified
1190 * link ID (with click credit to the appropriate {links_node} record) or
1191 * to the first [or only] link for the specified node, again with appropriate
1192 * click credit.
1193 *
1194 * It is valid for $nid to be zero, if $lid is nonzero. In that case, the
1195 * function will redirect to the URL specified by the link, but will not
1196 * tally the link for a particular node.
1197 *
1198 * Unlike other instances of $module, here we default to an empty string to
1199 * fetch any link regardless of context. Callers can override that behavior,
1200 * and indeed almost always will want to do so.
1201 *
1202 * Returns TRUE if a redirect was actually sent to the browser, FALSE if not.
1203 */
1204 function links_goto_bynode($nid, $lid=0, $module='') {
1205 if ($nid == 0 && $lid > 0) {
1206 // Goto a link without crediting any specific node
1207 $link = array(links_get_link($lid));
1208 } else {
1209 // Look it up by node ID
1210 $link =& links_load_links_for_node($nid, $module, $lid, TRUE);
1211 }
1212 if (! is_array($link)) {
1213 return FALSE;
1214 }
1215 return links_goto_link($link[0], $module);
1216 }
1217
1218 /**
1219 * Given a link ID or the URL or its MD5 hash, redirects to that location and tallies one
1220 * click in the links_node table.
1221 *
1222 * If $module is not specified, the redirection will still work as long as
1223 * the link spec is valid, but the outbound click won't be tallied in the
1224 * database. $module is needed for tallying because it is legal for the
1225 * same node to refer to the same link from multiple module contexts.
1226 *
1227 * Returns TRUE if a redirect was actually sent to the browser, FALSE if not.
1228 */
1229 function links_goto($link_spec, $module='') {
1230 $link = links_get_link($link_spec);
1231 if (! is_array($link)) {
1232 return FALSE;
1233 }
1234 return links_goto_link($link, $module);
1235 }
1236
1237 /**
1238 * Given a link record array, redirects to the URL contained in the array.
1239 *
1240 * Returns TRUE if a redirect was actually sent to the browser, FALSE if not.
1241 */
1242 function links_goto_link($link, $module='') {
1243 if ($link['lid']) {
1244 # We can only tally the click if we know the associated module, because the
1245 # same link could be re