/[drupal]/contributions/modules/relatedlinks/relatedlinks.module
ViewVC logotype

Contents of /contributions/modules/relatedlinks/relatedlinks.module

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


Revision 1.58 - (show annotations) (download) (as text)
Sun Feb 17 17:40:45 2008 UTC (21 months, 1 week ago) by karthik
Branch: MAIN
CVS Tags: DRUPAL-5--2-0, HEAD
Branch point for: DRUPAL-5--2
Changes since 1.57: +6 -2 lines
File MIME type: text/x-php
Sync with D5.
1 <?php
2 // $Id: relatedlinks.module,v 1.57 2007/08/10 20:17:15 karthik Exp $
3
4 /**
5 * @file
6 * The relatedlinks module enables nodes to display related URLs to the user via
7 * blocks.
8 */
9
10 /**
11 * @todo
12 * 1. Decide on how to handle existing links when a node type is dissociated
13 * from this module or a prerequisite module is disabled.
14 * 2. As per Moshe's suggestion, this module will also be handy in ensuring that
15 * dead links to a deleted page can be weeded out.
16 * 3. Add a backlinks block [#97346].
17 * 4. Manual links UI could use some JS goodness.
18 */
19
20 // Define mnemonics to indicate the types of links stored in the relatedlinks
21 // table [type field].
22 define('RELATEDLINKS_PARSED', 1);
23 define('RELATEDLINKS_MANUAL', 2);
24 define('RELATEDLINKS_DISCOVERED', 3);
25
26 /**
27 * Implementation of hook_help().
28 */
29 function relatedlinks_help($section) {
30 switch ($section) {
31 case 'admin/settings/relatedlinks':
32 return t('Select the types of links to store and display.');
33 case 'admin/settings/relatedlinks/discovered':
34 return t('Define the criteria by which discovered links are to be calculated.');
35 case 'admin/help#relatedlinks':
36 case 'admin/help#relatedlinks/discovered':
37 return t('The relatedlinks module enables nodes to display related URLs to
38 the user via blocks. Related links can be defined in 3 ways:
39 <ul>
40 <li>Parsed links: links that are retrieved from the body of a node.</li>
41 <li>Manual links: links that are added manually.</li>
42 <li>Discovered links: links that are discovered by the module using various criteria,
43 including the category terms of a node and suggestions provided by the search module
44 (when enabled).</li>
45 </ul>
46 <p>The relatedlinks module allows for flexibility in creating blocks for each type
47 of relatedlinks or creating blocks for a combination of link types.</p>
48 Links:
49 <ul>
50 <li>Related links configuration: admin/settings/relatedlinks</li>
51 <li>Block configuration: admin/build/block</li>
52 <li>Project URL: http://drupal.org/project/relatedlinks</li>
53 </ul>');
54 }
55 }
56
57 /**
58 * Implementation of hook_menu().
59 */
60 function relatedlinks_menu($may_cache) {
61 $items = array();
62
63 if ($may_cache) {
64 $items[] = array(
65 'path' => 'admin/settings/relatedlinks',
66 'title' => t('Related links'),
67 'description' => t('Configure related links settings.'),
68 'callback' => 'drupal_get_form',
69 'callback arguments' => array('_relatedlinks_settings_form'),
70 'access' => user_access('administer related links')
71 );
72 $items[] = array(
73 'path' => 'admin/settings/relatedlinks/configure',
74 'title' => t('Settings'),
75 'callback' => 'drupal_get_form',
76 'callback arguments' => array('_relatedlinks_settings_form'),
77 'access' => user_access('administer related links'),
78 'type' => MENU_DEFAULT_LOCAL_TASK
79 );
80 $items[] = array(
81 'path' => 'admin/settings/relatedlinks/discovered',
82 'title' => t('Discovered links'),
83 'callback' => 'drupal_get_form',
84 'callback arguments' => array('_relatedlinks_discovered_settings_form'),
85 'access' => user_access('administer related links'),
86 'type' => MENU_LOCAL_TASK,
87 'weight' => 2
88 );
89 }
90
91 return $items;
92 }
93
94 /**
95 * Implementation of hook_perm().
96 */
97 function relatedlinks_perm() {
98 return array('administer related links', 'add related links');
99 }
100
101 /**
102 * Implementation of hook_block().
103 * Provide a block to show the related links.
104 *
105 * @todo Add caching support.
106 */
107 function relatedlinks_block($op = 'list', $delta = 0) {
108 switch ($op) {
109 case 'list':
110 $blocks = array();
111
112 $link_types = variable_get('relatedlinks_types', _relatedlinks_get_type_defaults());
113 $top = TRUE;
114 foreach ($link_types as $type => $config) {
115 if ($config['enabled'] && ($config['block'] || $top)) {
116 $top = FALSE;
117 $blocks[$type]['info'] = t('Related links: ') . check_plain($config['name']);
118 }
119 }
120
121 return $blocks;
122 case 'view':
123 // This is only valid if we're looking at a node now.
124 if (arg(0) == 'node' && is_numeric(arg(1))) {
125 $block = array();
126 $node = node_load(arg(1));
127 // Only display if node type is associated with this module.
128 if ($node && in_array($node->type, variable_get('relatedlinks_node_types', _relatedlinks_node_get_types(TRUE)))) {
129 $link_types = variable_get('relatedlinks_types', _relatedlinks_get_type_defaults());
130 $append = FALSE;
131 $links = array();
132 foreach ($link_types as $type => $config) {
133 // Only worry about enabled types.
134 if ($config['enabled']) {
135 if ($type == RELATEDLINKS_DISCOVERED && (($delta == $type) || ($append && !$config['block'])) &&
136 !isset($node->relatedlinks[RELATEDLINKS_DISCOVERED])) {
137 // Refresh discovered links. This is being done here rather than
138 // 'hook_nodeapi load' due to weight conflicts with the taxonomy
139 // module.
140 $node->relatedlinks[RELATEDLINKS_DISCOVERED] = _relatedlinks_update_discovered_links($node);
141 }
142
143 // Current block.
144 if ($delta == $type) {
145 $block['subject'] = check_plain($config['title']);
146 $links = isset($node->relatedlinks[$type]) ? array_merge($links, $node->relatedlinks[$type]) : $links;
147 // Allow other types to append links to this block if needed.
148 $append = TRUE;
149 }
150 else if ($append) {
151 if ($config['block']) {
152 break;
153 }
154 else {
155 $links = isset($node->relatedlinks[$type]) ? array_merge($links, $node->relatedlinks[$type]) : $links;
156 }
157 }
158 }
159 }
160 $links = _relatedlinks_filter($links);
161 if (!empty($links)) {
162 $block['content'] = theme('relatedlinks', $links);
163 }
164
165 return $block;
166 }
167 }
168 }
169 }
170
171 /**
172 * Implementation of hook_nodeapi().
173 *
174 * @TODO: Move the parsed links filtering into a filter_process(). The current
175 * implementation is not PHP filter friendly.
176 */
177 function relatedlinks_nodeapi(&$node, $op, $arg) {
178 if (in_array($node->type, variable_get('relatedlinks_node_types', _relatedlinks_node_get_types(TRUE)))) {
179 switch ($op) {
180 case 'load':
181 $node->relatedlinks = _relatedlinks_get_links($node->nid);
182 break;
183 default:
184 // The delete op should be allowed to account for admins who do not have
185 // the 'add related links' permission.
186 if (user_access('add related links') || $op == 'delete') {
187 switch ($op) {
188 case 'delete':
189 _relatedlinks_delete_links($node->nid);
190 _relatedlinks_delete_tracker($node->nid);
191 break;
192 case 'update':
193 _relatedlinks_delete_links($node->nid);
194 _relatedlinks_delete_tracker($node->nid);
195 // Fall through.
196 case 'insert':
197 // Handle manual links.
198 if (_relatedlinks_get_type_property(RELATEDLINKS_MANUAL, 'enabled')) {
199 preg_match_all('#\s*([^\ \s]+)\ *(.*)$#im', $node->relatedlinks_fieldset['relatedlinks'], $matches);
200 $links = _relatedlinks_check_links($matches[1], $matches[2]);
201 _relatedlinks_add_links($node->nid, $links, RELATEDLINKS_MANUAL);
202 }
203
204 // Handle any submitted keywords. Discovered links are calculated
205 // and cached (as necessary) in hook_block.
206 $discovered = variable_get('relatedlinks_discovered', _relatedlinks_get_discovered_defaults());
207 // Existing keywords are removed if the keywords functionality is
208 // disabled.
209 $keywords = '';
210 if (_relatedlinks_get_type_property(RELATEDLINKS_DISCOVERED, 'enabled') &&
211 in_array('search', $discovered['ranking']) && $discovered['keywords']) {
212 $keywords = $node->relatedlinks_fieldset['keywords'];
213 }
214 _relatedlinks_insert_tracker($node->nid, $keywords);
215
216 // Handle parsed links.
217 if (_relatedlinks_get_type_property(RELATEDLINKS_PARSED, 'enabled')) {
218 // Run node body through check_markup to ensure that only
219 // processed output is being parsed for links.
220 $node->body = check_markup($node->body, $node->format, FALSE);
221
222 // Previous versions of this module attempted to retain other tag
223 // attributes. This has been curtailed due to changes in the DB
224 // structure.
225 // Regex adapted from syscrusher's links package, which in turn
226 // was adapted from a post on php.net by "martin at vertikal dot dk".
227 // Any updates to this regex also needs to be applied to the
228 // .install file update.
229 preg_match_all("!<\s*a\s*href\s*=\s*(?:\"([^\">]+)\"[^>]*|([^\" >]+?)[^>]*)>(.*)\s*<\s*/\s*a\s*>!Uis", $node->body, $matches);
230 $links = _relatedlinks_check_links($matches[1], $matches[3]);
231 _relatedlinks_add_links($node->nid, $links, RELATEDLINKS_PARSED);
232 }
233 }
234 }
235 }
236 }
237 }
238
239 /**
240 * Implementation of hook_form_alter().
241 */
242 function relatedlinks_form_alter($form_id, &$form) {
243 if (user_access('add related links') && isset($form['type']) &&
244 ($form['type']['#value'] .'_node_form' == $form_id)) {
245
246 $node = $form['#node'];
247 if (!in_array($node->type, variable_get('relatedlinks_node_types', _relatedlinks_node_get_types(TRUE)))) {
248 return;
249 }
250 $form['relatedlinks_fieldset'] = array(
251 '#type' => 'fieldset',
252 '#title' => t('Related links'),
253 '#collapsible' => TRUE,
254 '#tree' => TRUE,
255 '#weight' => 30
256 );
257
258 if (_relatedlinks_get_type_property(RELATEDLINKS_MANUAL, 'enabled')) {
259 $relatedlinks = '';
260 if (isset($node->relatedlinks[RELATEDLINKS_MANUAL])) {
261 foreach ($node->relatedlinks[RELATEDLINKS_MANUAL] as $link) {
262 $relatedlinks .= $link['url'] .' '. $link['title'] ."\n";
263 }
264 }
265
266 $form['relatedlinks_fieldset']['relatedlinks'] = array(
267 '#type' => 'textarea',
268 '#title' => t('Related links'),
269 '#default_value' => $relatedlinks,
270 '#rows' => 3,
271 '#description' => t('To manually define links to related material, enter one URL and title (separated by a space) per line. For example: "<code>http://www.example.com Clickable Text</code>". Alternately, you can enter an internal site address. For example: "<code>about About Us</code>". If no title is submitted, the URL itself will be used as the title.')
272 );
273
274 $form['relatedlinks_fieldset']['#collapsed'] = empty($relatedlinks);
275
276 $path = '/'. drupal_get_path('module', 'relatedlinks');
277 drupal_add_js($path .'/relatedlinks.js');
278 drupal_add_css($path .'/relatedlinks.css');
279 }
280 $discovered = variable_get('relatedlinks_discovered', _relatedlinks_get_discovered_defaults());
281 if (_relatedlinks_get_type_property(RELATEDLINKS_DISCOVERED, 'enabled') &&
282 in_array('search', $discovered['ranking']) && $discovered['keywords']) {
283
284 $form['relatedlinks_fieldset']['keywords'] = array(
285 '#type' => 'textfield',
286 '#title' => t('Keywords'),
287 '#description' => t('Enter search keywords that will aid in finding related content.'),
288 );
289
290 if ($track = _relatedlinks_get_tracker($node->nid)) {
291 $form['relatedlinks_fieldset']['keywords']['#default_value'] = $track['keywords'];
292 $form['relatedlinks_fieldset']['#collapsed'] = $form['relatedlinks_fieldset']['#collapsed'] && empty($track['keywords']);
293 }
294 }
295 if (!isset($form['relatedlinks_fieldset']['relatedlinks']) && !isset($form['relatedlinks_fieldset']['keywords'])) {
296 unset($form['relatedlinks_fieldset']);
297 }
298 }
299 }
300
301 /**
302 * Implementation of hook_cron().
303 */
304 function relatedlinks_cron() {
305 $discovered = variable_get('relatedlinks_discovered', _relatedlinks_get_discovered_defaults());
306 if (_relatedlinks_get_type_property(RELATEDLINKS_DISCOVERED, 'enabled') && $discovered['cron']) {
307 $updated = time() - ($discovered['cron'] * 24 * 60 * 60);
308 // Select all stale nodes.
309 $result = db_query("SELECT nid FROM {relatedlinks_tracker} WHERE updated != 0 AND updated < %d", $updated);
310 $nids = array();
311 while ($node = db_fetch_array($result)) {
312 $nids[] = $node['nid'];
313 }
314 if (!empty($nids)) {
315 $nid_args = implode(', ', $nids);
316 // Remove stale discovered links.
317 db_query("DELETE FROM {relatedlinks} WHERE nid IN (". $nid_args .") AND type = %d", RELATEDLINKS_DISCOVERED);
318 // Reset updated field to 0. This will force a recalculation of discovered links.
319 db_query("UPDATE {relatedlinks_tracker} SET updated = 0 WHERE nid IN (". $nid_args .")");
320 }
321 }
322 }
323
324 /**
325 * Menu Callback: relatedlinks module settings form.
326 */
327 function _relatedlinks_settings_form() {
328 $types = variable_get('relatedlinks_types', _relatedlinks_get_type_defaults());
329
330 $form['relatedlinks_types'] = array(
331 '#type' => 'fieldset',
332 '#tree' => TRUE,
333 '#title' => t('Link Types'),
334 '#theme' => 'relatedlinks_types_table',
335 '#description' => t('<p>These are the controls for specific link types.
336 <ul>
337 <li>Link type: The type of link.</li>
338 <li>Enabled: Should links of this type be displayed?</li>
339 <li>Block: Should links of this type be displayed in a separate block or
340 added to the previous related links block? Newly enabled blocks will also need to
341 be activated on the <a href="!block">blocks page</a>.</li>
342 <li>Title: An optional title for the block.</li>
343 <li>Max: The maximum number of links to be displayed.</li>
344 <li>Weight: Display order priority for links. Heavier types sink to the
345 bottom.</li>
346 </ul>
347 </p>', array('!block' => url('admin/build/block'))),
348 '#collapsible' => TRUE,
349 '#collapsed' => FALSE
350 );
351
352 foreach ($types as $type_name => $type_values) {
353 $form['relatedlinks_types'][$type_name] = array('#weight' => $type_values['weight']);
354
355 $form['relatedlinks_types'][$type_name]['info'] = array('#value' => $type_values['name']);
356 // The name key is used during block configuration.
357 $form['relatedlinks_types'][$type_name]['name'] = array(
358 '#type' => 'value',
359 '#value' => $type_values['name']
360 );
361 $form['relatedlinks_types'][$type_name]['enabled'] = array(
362 '#type' => 'checkbox',
363 '#default_value' => $type_values['enabled']
364 );
365 $form['relatedlinks_types'][$type_name]['block'] = array(
366 '#type' => 'checkbox',
367 '#default_value' => $type_values['block']
368 );
369 $form['relatedlinks_types'][$type_name]['title'] = array(
370 '#type' => 'textfield',
371 '#size' => 20,
372 '#default_value' => $type_values['title']
373 );
374 $form['relatedlinks_types'][$type_name]['max'] = array(
375 '#type' => 'select',
376 '#options' => range(0, 25),
377 '#default_value' => $type_values['max']
378 );
379 $form['relatedlinks_types'][$type_name]['weight'] = array(
380 '#type' => 'weight',
381 '#delta' => 10,
382 '#default_value' => $type_values['weight']
383 );
384 }
385
386 $form['content_types'] = array(
387 '#type' => 'fieldset',
388 '#title' => t('Content types'),
389 '#description' => t('Select the content types to associate with this module.
390 Related links blocks and forms will only appear for these nodes.'),
391 '#collapsible' => TRUE,
392 '#collapsed' => TRUE
393 );
394 $form['content_types']['relatedlinks_node_types'] = array(
395 '#type' => 'checkboxes',
396 '#title' => t('Content types'),
397 '#options' => _relatedlinks_node_get_types(),
398 '#default_value' => variable_get('relatedlinks_node_types', _relatedlinks_node_get_types(TRUE))
399 );
400
401 $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
402
403 return $form;
404 }
405
406 /**
407 * Process relatedlinks settings form submissions.
408 */
409 function _relatedlinks_settings_form_submit($form_id, $form_values) {
410 // Sort on 'weight'.
411 uasort($form_values['relatedlinks_types'], '_relatedlinks_compare');
412
413 // Store the entire relatedlinks_types table.
414 variable_set('relatedlinks_types', $form_values['relatedlinks_types']);
415
416 $node_types = array_filter($form_values['relatedlinks_node_types']);
417 variable_set('relatedlinks_node_types', array_keys($node_types));
418
419 // Block table needs to be reset.
420 _block_rehash();
421 cache_clear_all();
422
423 drupal_set_message(t('Configuration settings saved. If necessary, relatedlinks
424 blocks can be configured via the <a href="!block-page">block configuration</a> page.', array('!block-page' => url('admin/build/block'))));
425 }
426
427 /**
428 * Menu callback: Discovered links settings form.
429 * @todo
430 * UI could use a better design. Perhaps the weighting system needs to be
431 * reintroduced.
432 */
433 function _relatedlinks_discovered_settings_form() {
434 if (!module_exists('taxonomy') && !module_exists('search')) {
435 drupal_set_message(t('The taxonomy and/or the search module are prerequisites
436 for discovered links. Please use the <a href="!modules">modules page</a> to enable them.', array('!modules' => url('admin/build/modules'))), 'error');
437 drupal_goto('admin/settings/relatedlinks');
438 }
439 $form['relatedlinks_discovered'] = array('#tree' => TRUE);
440 $form['relatedlinks_discovered']['criteria'] = array(
441 '#type' => 'fieldset',
442 '#title' => t('Criteria'),
443 '#description' => t('Define how discovered links are calculated. The following
444 criteria are applied to results derived from the taxonomy and/or search module
445 (if selected) on a best effort basis.'),
446 '#collapsible' => TRUE,
447 '#collapsed' => FALSE
448 );
449
450 $discovered = variable_get('relatedlinks_discovered', _relatedlinks_get_discovered_defaults());
451 $criteria = array(
452 'taxonomy' => t('Results based on taxonomy terms (if enabled).'),
453 'search' => t('Results from the search module (if enabled).'),
454 'node' => t('Prefer results of the same content type.'),
455 'user' => t('Prefer results from the same author.'),
456 'date' => t('Prefer newer results.'),
457 'comments' => t('Prefer results with comments.')
458 );
459 $form['relatedlinks_discovered']['criteria']['ranking'] = array(
460 '#type' => 'checkboxes',
461 '#parents' => array('relatedlinks_discovered', 'ranking'),
462 '#options' => $criteria,
463 '#default_value' => $discovered['ranking']
464 );
465
466 if (module_exists('taxonomy')) {
467 $voptions = array();
468 $vocabularies = taxonomy_get_vocabularies();
469 foreach ($vocabularies as $vid => $vname) {
470 $voptions[$vid] = check_plain($vname->name);
471 }
472 $form['relatedlinks_discovered']['taxonomy_options'] = array(
473 '#type' => 'fieldset',
474 '#title' => t('Taxonomy options'),
475 '#description' => t('Restrict results to the following vocabularies.'),
476 '#collapsible' => TRUE,
477 '#collapsed' => TRUE
478 );
479 $form['relatedlinks_discovered']['taxonomy_options']['vocabularies'] = array(
480 '#type' => 'checkboxes',
481 '#title' => t('Vocabularies'),
482 '#parents' => array('relatedlinks_discovered', 'vocabularies'),
483 '#options' => $voptions,
484 '#default_value' => isset($discovered['vocabularies']) ? $discovered['vocabularies'] : array_keys($vocabularies)
485 );
486 }
487
488 $form['relatedlinks_discovered']['content_types'] = array(
489 '#type' => 'fieldset',
490 '#title' => t('Content types'),
491 '#description' => t('Restrict results to the following content types.'),
492 '#collapsible' => TRUE,
493 '#collapsed' => TRUE
494 );
495 $form['relatedlinks_discovered']['content_types']['node_types'] = array(
496 '#type' => 'checkboxes',
497 '#title' => t('Content types'),
498 '#parents' => array('relatedlinks_discovered', 'node_types'),
499 '#options' => _relatedlinks_node_get_types(),
500 '#default_value' => isset($discovered['node_types']) ? $discovered['node_types'] : _relatedlinks_node_get_types(TRUE)
501 );
502
503 $form['relatedlinks_discovered']['search_options'] = array(
504 '#type' => 'fieldset',
505 '#title' => t('Search options'),
506 '#description' => t('Keywords can be specified for each associated node that
507 can be used to aid in the calculation of results using the search module.'),
508 '#collapsible' => TRUE,
509 '#collapsed' => TRUE
510 );
511 $form['relatedlinks_discovered']['search_options']['keywords'] = array(
512 '#type' => 'checkbox',
513 '#title' => t('Display a search keywords field.'),
514 '#description' => t('The textfield will appear on node forms. Provided keywords
515 will be used to discover related content and will generally provide better results.
516 Advanced search queries can also be entered as specfied by the search modules.'),
517 '#parents' => array('relatedlinks_discovered', 'keywords'),
518 '#default_value' => $discovered['keywords']
519 );
520 $form['relatedlinks_discovered']['cron'] = array(
521 '#type' => 'fieldset',
522 '#title' => t('Cron'),
523 '#description' => t('Due to the dynamic nature of discovered links, it is recommended
524 that they be periodically purged and recalculated.'),
525 '#collapsible' => TRUE,
526 '#collapsed' => TRUE
527 );
528
529 $form['relatedlinks_discovered']['cron']['refresh'] = array(
530 '#type' => 'select',
531 '#title' => t('Automatically update discovered links older than'),
532 '#description' => t('This feature requires <a href="!cron">cron</a> to be enabled.', array('!cron' => url('admin/logs/status'))),
533 '#parents' => array('relatedlinks_discovered', 'cron'),
534 '#options' => array(
535 1 => t('One day'),
536 3 => t('Three days'),
537 7 => t('One week'),
538 28 => t('Four weeks'),
539 0 => t('Never update automatically')
540 ),
541 '#default_value' => $discovered['cron']
542 );
543
544 $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
545
546 return $form;
547 }
548
549 /**
550 * Validate discovered links settings form submissions.
551 */
552 function _relatedlinks_discovered_settings_form_validate($form_id, $form_values) {
553 if ($form_values['relatedlinks_discovered']['ranking']['taxonomy'] === $form_values['relatedlinks_discovered']['ranking']['search'] &&
554 $form_values['relatedlinks_discovered']['ranking']['taxonomy'] === 0) {
555
556 $error = t('The discovered links feature requires either taxonomy or search results to be enabled.');
557 form_set_error('relatedlinks_discovered][criteria][ranking', $error);
558 }
559 if ($form_values['relatedlinks_discovered']['ranking']['taxonomy'] === 'taxonomy' && !module_exists('taxonomy')) {
560 form_set_error('relatedlinks_discovered][criteria][ranking', t('The taxonomy module needs to be enabled for taxonomy results to be calculated.'));
561 }
562 if ($form_values['relatedlinks_discovered']['ranking']['search'] === 'search' && !module_exists('search')) {
563 form_set_error('relatedlinks_discovered][criteria][ranking', t('The search module needs to be enabled for taxonomy results to be calculated.'));
564 }
565 }
566
567 /**
568 * Process discovered links settings form submissions.
569 */
570 function _relatedlinks_discovered_settings_form_submit($form_id, $form_values) {
571 $indices = array('ranking', 'vocabularies', 'node_types');
572 foreach ($indices as $index) {
573 if (isset($form_values['relatedlinks_discovered'][$index])) {
574 $form_values['relatedlinks_discovered'][$index] = array_filter($form_values['relatedlinks_discovered'][$index]);
575 $form_values['relatedlinks_discovered'][$index] = array_keys($form_values['relatedlinks_discovered'][$index]);
576 }
577 }
578
579 variable_set('relatedlinks_discovered', $form_values['relatedlinks_discovered']);
580 }
581
582 /*
583 * Helper function to sort related link types by weight, enabled status and
584 * block status respectively.
585 */
586 function _relatedlinks_compare($a, $b) {
587 if ($a['weight'] == $b['weight']) {
588 if ($a['enabled'] == $b['enabled']) {
589 return ($a['block']) ? -1 : 1;
590 }
591 else {
592 return ($a['enabled']) ? -1 : 1;
593 }
594 }
595
596 return ($a['weight'] < $b['weight']) ? -1 : 1;
597 }
598
599 /**
600 * Create a data structure for link types data. To add a new link type, simply
601 * add a new array to the structure below.
602 *
603 * @return
604 * Default settings for the relatedlinks type associations.
605 */
606 function _relatedlinks_get_type_defaults() {
607 static $defaults = NULL;
608
609 if (!isset($defaults)) {
610 $defaults = array();
611 $defaults[RELATEDLINKS_MANUAL] = array(
612 'name' => t('Manual links'),
613 'enabled' => TRUE,
614 'block' => TRUE,
615 'weight' => 1,
616 'title' => t('Recommended links'),
617 'max' => 5
618 );
619 $defaults[RELATEDLINKS_PARSED] = array(
620 'name' => t('Parsed links'),
621 'enabled' => TRUE,
622 'block' => FALSE,
623 'weight' => 2,
624 'title' => t('Content links'),
625 'max' => 5
626 );
627 $defaults[RELATEDLINKS_DISCOVERED] = array(
628 'name' => t('Discovered links'),
629 'enabled' => FALSE,
630 'block' => TRUE,
631 'weight' => 3,
632 'title' => t('Similar links'),
633 'max' => 5
634 );
635 }
636
637 return $defaults;
638 }
639
640 /**
641 * Retrieve a link type property.
642 *
643 * @param $type
644 * The link type as declared by the type constants.
645 * @param $property
646 * The property to return.
647 * @return
648 * The property value.
649 */
650 function _relatedlinks_get_type_property($type, $property) {
651 $link_types = variable_get('relatedlinks_types', _relatedlinks_get_type_defaults());
652
653 return $link_types[$type][$property];
654 }
655
656 /**
657 * Retrieve default settings for the discovered links settings page.
658 *
659 * @return
660 * An array of default values.
661 */
662 function _relatedlinks_get_discovered_defaults() {
663 // The default array doesn't provide data for taxonomy vocabularies and node
664 // types for performance reasons.
665 $defaults = array(
666 'keywords' => TRUE,
667 'ranking' => array('taxonomy', 'date'),
668 'cron' => 7
669 );
670
671 return $defaults;
672 }
673
674 /**
675 * Insert related links into the database.
676 *
677 * @param $nid
678 * Node ID of the node to add links for.
679 * @param $links
680 * An associative array of URLs and titles.
681 * @param $type
682 * The type of the links being inserted as denoted by the type constants.
683 */
684 function _relatedlinks_add_links($nid, $links, $type) {
685 foreach ($links as $link) {
686 db_query("INSERT INTO {relatedlinks} (nid, url, title, type) VALUES (%d, '%s', '%s', %d)", $nid, $link['url'], trim($link['title']), $type);
687 }
688 }
689
690 /**
691 * Delete related links associated with a node from the database.
692 *
693 * @param $nid
694 * Node ID of the node to delete links for.
695 * @param $type
696 * The type of the links to delete as denoted by the type constants.
697 *
698 */
699 function _relatedlinks_delete_links($nid, $type = NULL) {
700 if (isset($type)) {
701 db_query('DELETE FROM {relatedlinks} WHERE nid = %d AND type = %d', $nid, $type);
702 }
703 else {
704 db_query('DELETE FROM {relatedlinks} WHERE nid = %d', $nid);
705 }
706 }
707
708 /**
709 * Retrieve related links from the database.
710 *
711 * @param $nid
712 * Node ID of the node to retrieve links for.
713 *
714 * @return $links
715 * An associative array of links arranged by link type.
716 */
717 function _relatedlinks_get_links($nid) {
718 $result = db_query('SELECT url, title, type FROM {relatedlinks} WHERE nid = %d ORDER BY lid', $nid);
719
720 $links = array();
721 while ($link = db_fetch_array($result)) {
722 if (!isset($links[$link['type']])) {
723 // Initialise array.
724 $links[$link['type']] = array();
725 }
726 $links[$link['type']][] = array('url' => $link['url'], 'title' => $link['title']);
727 }
728
729 return $links;
730 }
731
732 /**
733 * Filter an array of related links. Check for duplicates and return as a list
734 * of URLs.
735 *
736 * @param $links
737 * Associative array of links.
738 *
739 * @return
740 * An array of HTML links.
741 *
742 * @TODO: Either fix url() to handle links starting with '/' or handle locally.
743 * Path aliasing is not available due to the current approach. This only affects
744 * parsed and manual links.
745 */
746 function _relatedlinks_filter($links) {
747 $urls = array();
748 $url_cache = array();
749
750 foreach ($links as $link) {
751 if (!in_array($link['url'], $url_cache)) {
752 if (empty($link['title'])) {
753 // URLs without a title display the URL.
754 $link['title'] = $link['url'];
755 }
756 $url_cache[] = $link['url'];
757 $urls[] = '<a href="'. check_url($link['url']) .'">'. check_plain($link['title']) .'</a>';
758 }
759 }
760
761 return $urls;
762 }
763
764 /**
765 * Check and return an updated list of discovered links.
766 *
767 * @param $node
768 * The node whose discovered links need to be checked and returned.
769 *
770 * @return $links
771 * An associative array of discovered links.
772 */
773 function _relatedlinks_update_discovered_links($node) {
774 $links = array();
775
776 $tracker = _relatedlinks_get_tracker($node->nid);
777
778 if (!$tracker || !$tracker['updated']) {
779 $links = _relatedlinks_get_discovered_links($node, $tracker['keywords']);
780
781 _relatedlinks_add_links($node->nid, $links, RELATEDLINKS_DISCOVERED);
782 if (!$tracker) {
783 // Insert tracker record for existing nodes that have been associated with
784 // this module.
785 _relatedlinks_insert_tracker($node->nid, '');
786 }
787 _relatedlinks_update_tracker_timestamp($node->nid, time());
788 }
789
790 return $links;
791 }
792
793 /**
794 * Retrieve information on the discovered links for a node from the track
795 * database.
796 *
797 * @param $nid
798 * Node ID of the node to get tracker information for.
799 *
800 * @return
801 * An associative array with a row from the tracker table, or FALSE if no
802 * data could be retrieved.
803 */
804 function _relatedlinks_get_tracker($nid) {
805 $result = db_query('SELECT * FROM {relatedlinks_tracker} WHERE nid = %d', $nid);
806
807 if ($track = db_fetch_array($result)) {
808 return $track;
809 }
810
811 return FALSE;
812 }
813
814 /**
815 * Insert a record into the relatedlinks_tracker table.
816 *
817 * @param $nid
818 * Node ID of the node to insert a tracker record for.
819 * @param $keywords
820 * Search module keywords that will allow discovered links to be calculated
821 * using the search module.
822 */
823 function _relatedlinks_insert_tracker($nid, $keywords) {
824 db_query("INSERT INTO {relatedlinks_tracker} (nid, keywords) VALUES (%d, '%s')", $nid, $keywords);
825 }
826
827 /**
828 * Insert a record into the relatedlinks_tracker table.
829 *
830 * @param $nid
831 * Node ID of the node whose tracker record is to be updated.
832 * @param $updated
833 * A UNIX timestamp indicating the time of last update (of discovered links
834 * for the node in question. 0 signifies that an update is yet to occur.
835 */
836 function _relatedlinks_update_tracker_timestamp($nid, $updated) {
837 db_query("UPDATE {relatedlinks_tracker} SET updated = %d WHERE nid = %d", $updated, $nid);
838 }
839
840 /**
841 * Delete a record from the relatedlinks_tracker table.
842 *
843 * @param $nid
844 * Node ID of the node whose tracker record is to be deleted.
845 */
846 function _relatedlinks_delete_tracker($nid) {
847 db_query('DELETE FROM {relatedlinks_tracker} WHERE nid = %d', $nid);
848 }
849
850 /**
851 * Helper function that caches node types.
852 *
853 * @param $keys
854 * A boolean variable that decides whether an associative array of node types
855 * is to be returned (or not).
856 *
857 * @return
858 * An associative or non-associative array of node types.
859 */
860 function _relatedlinks_node_get_types($keys = FALSE) {
861 static $node_types = NULL, $key_array = NULL;
862
863 if (!isset($node_types)) {
864 $node_types = node_get_types('names');
865 foreach ($node_types as $type => $name) {
866 $node_types[$type] = check_plain($name);
867 }
868 $key_array = array_keys($node_types);
869 }
870
871 return $keys ? $key_array : $node_types;
872 }
873
874 /**
875 * Check links for duplicates and return as an associative array.
876 *
877 * @param $url_matches
878 * An array of URLs.
879 * @param $title_matches
880 * An array of titles (corresponding to the $url_matches array).
881 *
882 * @return $links
883 * An array of distinct URL and title pairs.
884 */
885 function _relatedlinks_check_links($url_matches, $title_matches) {
886 $urls = array();
887 $links = array();
888 // Check URLs for duplicates.
889 foreach ($url_matches as $index => $url) {
890 $url = rtrim($url, '/ ');
891 if (!in_array($url, $urls)) {
892 $urls[] = $url;
893 // The title is trimmed in _relatedlinks_add_links due to
894 // inadequacies in the current regex.
895 $links[] = array('url' => $url, 'title' => $title_matches[$index]);
896 }
897 }
898
899 return $links;
900 }
901
902 /**
903 * Calculate, collate and return the discovered links for a node.
904 *
905 * @param $node
906 * The node whose discovered links are to be calculated.
907 * @param $keywords
908 * The search module keywords associated with the node.
909 */
910 function _relatedlinks_get_discovered_links($node, $keywords) {
911 $discovered = variable_get('relatedlinks_discovered', _relatedlinks_get_discovered_defaults());
912 $max = _relatedlinks_get_type_property(RELATEDLINKS_DISCOVERED, 'max');
913 $node_types = isset($discovered['node_types']) ? $discovered['node_types'] : array();
914
915 $user = in_array('user', $discovered['ranking']) ? $node->uid : FALSE;
916 $date = in_array('date', $discovered['ranking']) ? TRUE : FALSE;
917 $comments = in_array('comments', $discovered['ranking']) ? TRUE : FALSE;
918
919 $taxonomy_links = $search_links = array();
920 if (module_exists('taxonomy') && in_array('taxonomy', $discovered['ranking'])) {
921 $vids = isset($discovered['vocabularies']) ? $discovered['vocabularies'] : array();
922 $tids = array_keys($node->taxonomy);
923 $taxonomy_links = _relatedlinks_taxonomy_select_nodes($node->nid, $tids, $max, $vids, $node_types, $date, $user, $comments);
924 }
925 if (module_exists('search') && in_array('search', $discovered['ranking'])) {
926 $search_links = _relatedlinks_search_select_nodes($node->nid, $keywords, $max, $node_types);
927 }
928
929 if ($keywords == '') {
930 $links = _relatedlinks_collate_links($taxonomy_links, $search_links, $user, $max);
931 }
932 else {
933 // If keywords are specified, priority is given to search results.
934 $links = _relatedlinks_collate_links($search_links, $taxonomy_links, $user, $max);
935 }
936
937 return $links;
938 }
939
940 /**
941 * Finds all nodes that match selected taxonomy conditions.
942 *
943 * @param $nid
944 * The Node ID of the node whose discovered links are being calculated.
945 * @param $tids
946 * An array of term IDs associated with the node.
947 * @param $limit
948 * The maximum number of links to discover. This is a user specified value.
949 * @param $vids
950 * An array of vocabulary IDs. Discovered links are limited to those belonging
951 * to these vocabularies (if non-empty).
952 * @param $node_types
953 * An array of node types. If non-empty, the discovered links are restricted
954 * to those of the specified node types.
955 * @param $date
956 * Boolean variable indicating if newer results are to be given preference.
957 * @param $comments
958 * Boolean variable indicating if nodes with higher numbers of comments are to
959 * be given preference.
960 *
961 * @return $links
962 * An array of discovered links related by taxonomy terms to the current node.
963 *
964 * @todo
965 * Handle term hierarchies (depth).
966 * Escape SQL input as an added precaution.
967 */
968 function _relatedlinks_taxonomy_select_nodes($nid, $tids, $limit, $vids, $node_types, $date, $comments) {
969 $links = array();
970
971 foreach ($tids as $index => $tid) {
972 $term = taxonomy_get_term($tid);
973 // Filter out terms that do not belong to the specified vids.
974 if (!empty($vids) && !in_array($term->vid, $vids)) {
975 unset($tids[$index]);
976 }
977 }
978
979 if (count($tids) > 0) {
980 $str_tids = implode(',', $tids);
981 $where[] = 'tn.tid IN ('. $str_tids .')';
982 if (!empty($node_types)) {
983 $where[] = "n.type IN ('". implode("', '", $node_types) ."')";
984 }
985
986 $where[] = 'n.status = 1';
987 $where[] = 'n.moderate = 0';
988 $where[] = 'n.nid != '. $nid;
989
990 $order[] = 'count DESC';
991 $order[] = 'sticky DESC';
992 if ($date) {
993 $order[] = 'created DESC';
994 }
995 if ($comments) {
996 $order[] = 'ncs.comment_count DESC';
997 }
998 $order[] = 'promote DESC';
999
1000 $sql = 'SELECT n.nid, n.title, n.type, n.uid, COUNT(tn.tid) as count, ncs.comment_count FROM {node} n INNER JOIN {term_node} tn USING (nid) LEFT JOIN {node_comment_statistics} ncs USING (nid) WHERE '. implode(' AND ', $where) .' GROUP BY n.nid ORDER BY '. implode(', ', $order) .' LIMIT '. $limit;
1001
1002 $result = db_query($sql);
1003 while ($node = db_fetch_object($result)) {
1004 // Exclude the current nid.
1005 if ($node->nid != $nid) {
1006 $links[$node->nid] = $node;
1007 }
1008 }
1009 }
1010
1011 return $links;
1012 }
1013
1014 /**
1015 * Finds all nodes that match selected taxonomy conditions.
1016 *
1017 * @param $nid
1018 * The Node ID of the node whose discovered links are being calculated.
1019 * @param $keywords
1020 * User specified keywords for the search module. These can be used just like
1021 * in the search form of the search module, including all advanced options such
1022 * as restricting by category or using Boolean operators to refine the query. If
1023 * not specified, an attempt is made to discern the key words pertaining to the
1024 * current node and use them in an OR search.
1025 * @param $limit
1026 * The maximum number of links to discover. This is a user specified value.
1027 * @param $node_types
1028 * An array of node types. If non-empty, the discovered links are restricted
1029 * to those of the specified node types. This is only used if keywords have not
1030 * been specified.
1031 *
1032 * @return $links
1033 * An array of discovered links provided by the search module.
1034 *
1035 * @todo
1036 * Hook search currently does not appear to have an option to specify the
1037 * number of results that can be returned.
1038 */
1039 function _relatedlinks_search_select_nodes($nid, $keywords, $limit, $node_types) {
1040 $links = array();
1041
1042 if ($keywords == '') {
1043 $result = db_query_range("SELECT DISTINCT(si.word) FROM {search_index} si INNER JOIN {search_total} st USING (word) WHERE si.sid = %d AND length(si.word) > %d AND si.type = 'node' ORDER BY st.count DESC, length(si.word) DESC", $nid, variable_get('minimum_word_size', 3), 0, 6);
1044 $words = array();
1045 while ($word = db_fetch_array($result)) {
1046 $words[] = $word['word'];
1047 }
1048 $keywords = implode(' OR ', $words);
1049 // Node type restrictions are only applicable if keywords have not been
1050 // specified. If otherwise, the user can specify the types with the keywords
1051 // as per the search module's syntax.
1052 // The !empty($keywords) has been added as just searching by type results in
1053 // a warning when hook_search is called.
1054 if (!empty($keywords) && !empty($node_types)) {
1055 $keywords .= ' type:'. implode(',', $node_types);
1056 }
1057 }
1058
1059 $keywords = trim($keywords);
1060
1061 if (module_hook('node', 'search') && !empty($keywords)) {
1062 $results = module_invoke('node', 'search', 'search', $keywords);
1063
1064 foreach ($results as $result) {
1065 $node = $result['node'];
1066 // Exclude the current nid.
1067 if ($node->nid != $nid) {
1068 $links[$node->nid] = $node;
1069 }
1070 }
1071 }
1072
1073 return $links;
1074 }
1075
1076 /**
1077 * Sort through the taxonomy and search link arrays and retrieve the discovered
1078 * links for a node.
1079 *
1080 * @param $set1
1081 * An array of discovered links.
1082 * @param $set2
1083 * Another array of discovered links.
1084 * @param $user
1085 * If content created by the author of the current node is to be given
1086 * preference, then this variable will contain the author's UID, else it is to
1087 * be set to FALSE.
1088 * @param $limit
1089 * The maximum number of links to return. This is a user specified value.
1090 *
1091 * @return $links
1092 * An array of discovered links.
1093 */
1094 function _relatedlinks_collate_links($set1, $set2, $user, $limit) {
1095 $links = $user_links = $set = array();
1096
1097 if (empty($set1) || empty($set2)) {
1098 $set = empty($set1) ? $set2 : $set1;
1099 }
1100 else {
1101 $set = array_intersect_assoc($set1, $set2) + array_diff_assoc($set1, $set2);
1102 }
1103
1104 foreach ($set as $node) {
1105 if ($user && $node->uid == $user) {
1106 $user_links[$node->nid] = array('url' => url('node/'. $node->nid), 'title' => $node->title);
1107 }
1108 else {
1109 $links[$node->nid] = array('url' => url('node/'. $node->nid), 'title' => $node->title);
1110 }
1111 }
1112 // Prioritise links submitted by the current node's user.
1113 $links = $user_links + $links;
1114 $links = array_slice($links, 0, $limit);
1115
1116 return $links;
1117 }
1118
1119 /**
1120 * Theme the related links configuration table.
1121 *
1122 * @param $form
1123 * The configuration form to theme.
1124 *
1125 * @return
1126 * A themed table of configuration settings.
1127 *
1128 * @todo
1129 * Get rid of the inline styles.
1130 */
1131 function theme_relatedlinks_types_table($form) {
1132 $types = element_children($form);
1133 foreach ($types as $type) {
1134 $rows[] = array(
1135 drupal_render($form[$type]['info']),
1136 drupal_render($form[$type]['enabled']),
1137 drupal_render($form[$type]['block']),
1138 drupal_render($form[$type]['title']),
1139 drupal_render($form[$type]['max']),
1140 drupal_render($form[$type]['weight'])
1141 );
1142 }
1143 $header = array(t('Link type'), t('Enabled'), t('Block'), t('Title'), t('Max'), t('Weight'));
1144
1145 $output = theme('table', $header, $rows, array('style' => 'width: 100%'));
1146
1147 return $output;
1148 }
1149
1150 /**
1151 * Theme the relatedlinks block output.
1152 *
1153 * @param $links
1154 * An array of links to theme.
1155 *
1156 * @return
1157 * A themed list of links.
1158 */
1159 function theme_relatedlinks($links = array()) {
1160 return theme('item_list', $links, $title);
1161 }

  ViewVC Help
Powered by ViewVC 1.1.2