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

Contents of /contributions/modules/category/category.module

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


Revision 1.162 - (show annotations) (download) (as text)
Wed Aug 5 04:52:53 2009 UTC (3 months, 3 weeks ago) by jaza
Branch: MAIN
CVS Tags: DRUPAL-6--2-0-RC1, HEAD
Changes since 1.161: +96 -89 lines
File MIME type: text/x-php
#158598 by JirkaRybka: a massive collection of bug fixes, D6 upgrades, and performance / usability improvements and other tasks for the category package. This commit includes patches from the following threads:
 - #370641: Port category_views to D6
 - #370633: Port category_breadcrumb to D6
 - #484624: Fix all broken category/container previews, and add category_display defaults
 - #481280: Generated menu items vs. menu administration and weights
 - #457688: Get rid of Menu wrapper, moving functionality to category_menu
 - #484084: Update README.txt and friends
 - #483978: Remove t() from database schema descriptions
 - #501378: PERFORMANCE! Central caching for category API functions
 - #521680: Missing argument error on category/### paths
 - #521714: Missing JavaScript file in Taxonomy wrapper
Lots and lots of thanks go to JirkaRybka for this monumental cleanup effort.
1 <?php
2 // $Id: category.module,v 1.161 2009/05/31 10:06:26 jaza Exp $
3
4 /**
5 * @file
6 * Allows users to structure their site and organize content with categories.
7 */
8
9 /**
10 * Implementation of hook_perm().
11 */
12 function category_perm() {
13 return array('administer categories');
14 }
15
16 /**
17 * Implementation of hook_theme()
18 */
19 function category_theme() {
20 return array(
21 'category_category_select' => array(
22 'arguments' => array('element' => NULL),
23 ),
24 'category_page' => array(
25 'arguments' => array('cids' => array(), 'result' => NULL),
26 ),
27 'category_overview_containers' => array(
28 'arguments' => array('form' => array()),
29 ),
30 'category_overview_categories' => array(
31 'arguments' => array('form' => array()),
32 ),
33 'category_wrapper_status' => array(
34 'arguments' => array('type' => NULL, 'status' => NULL),
35 ),
36 );
37 }
38
39 /**
40 * Implementation of hook_flush_caches()
41 */
42 function category_flush_caches() {
43 return array('cache_category');
44 }
45
46 /**
47 * Implementation of hook_link().
48 *
49 * This hook is extended with $type = 'categories' to allow themes to
50 * print lists of categories associated with a node. Themes can print category
51 * links with:
52 *
53 * if (module_exists('category')) {
54 * $categories = category_link('categories', $node);
55 * print theme('links', $categories);
56 * }
57 */
58 function category_link($type, $node = NULL, $teaser = FALSE) {
59 $links = array();
60
61 // 'Add child' links, a la book.module.
62 if ($type == 'node' && isset($node->category) && !$teaser) {
63 if ($node->status == 1 && $node->category['depth'] < MENU_MAX_DEPTH) {
64 foreach (node_get_types() as $node_type) {
65 $behavior = variable_get('category_behavior_'. $node_type->type, 0);
66 if (!empty($behavior) && node_access('create', $node_type->type)) {
67 $allowed_containers = variable_get(
68 'category_allowed_containers_'. $node_type->type, array());
69 $print_link = FALSE;
70 if ($node->category['behavior'] == 'container') {
71 // If the current node is a container, and if the node type of the
72 // link is a container, then always print a link.
73 if ($behavior == 'container') {
74 $print_link = TRUE;
75 }
76 else {
77 // If the current node is a container, but the node type of the
78 // link is a category, then print a link only if the current node
79 // is an allowed container for the node type in question.
80 if (empty($allowed_containers) || in_array($node->nid, $allowed_containers)) {
81 $print_link = TRUE;
82 }
83 }
84 }
85 else {
86 // If the current node is a category, and if the node type of the
87 // link is a container, then always print a link.
88 if ($behavior == 'container') {
89 $print_link = TRUE;
90 }
91 else {
92 $container = category_get_container($node->category['cnid']);
93 if (!$container->tags) {
94 // If the current node is a category, and the node type of the
95 // link is also a category, then print a link only if the
96 // container of the current node is an allowed container for
97 // the node type in question.
98 if ($container->hierarchy && (empty($allowed_containers) || in_array($node->category['cnid'], $allowed_containers))) {
99 $print_link = TRUE;
100 }
101 // If the current node and the node type of the link are both
102 // categories, but the container of the current node is NOT an
103 // allowed container for the node type, then print a link only
104 // if one of the allowed containers for the node type in
105 // question has the container of the current node as an allowed
106 // parent.
107
108 // Now say that backwards. ;-)
109 else {
110 foreach ($allowed_containers as $allowed_cnid) {
111 $allowed_container = category_get_container($allowed_cnid);
112 if (!empty($allowed_container->allowed_parent) && $allowed_container->allowed_parent == $container->cid) {
113 $print_link = TRUE;
114 break;
115 }
116 }
117 }
118 }
119 }
120 }
121
122 if ($print_link) {
123 $links['category_add_'. $node_type->type] = array(
124 'title' => t('Add child @type', array('@type' => drupal_strtolower($node_type->name))),
125 'href' => 'node/add/'. str_replace('_', '-', check_plain($node_type->type)),
126 'query' => 'parent='. $node->nid,
127 );
128 }
129 }
130 }
131 }
132
133 return $links;
134 }
135
136 // Assigned category links, a la taxonomy.module.
137 if ($type == 'categories' && $node != NULL) {
138 // If previewing, the categories must be converted to objects first.
139 if ($node->build_mode == NODE_BUILD_PREVIEW) {
140 $node->categories = category_preview_categories($node);
141 }
142 if (isset($node->categories) && is_array($node->categories)) {
143 foreach ($node->categories as $category) {
144 // During preview the free tagging categories are in an array unlike the
145 // other categories which are objects. So we have to check if a $category
146 // is an object or not.
147 if (is_object($category)) {
148 $category_display = NULL;
149 if (module_exists('category_display')) {
150 $category_display = category_display_get_container($category->cnid);
151 }
152 if (!isset($category_display) || !empty($category_display->nodelinks)) {
153 $links['category_cat_'. $category->cid] = array(
154 'title' => $category->title,
155 'href' => category_category_path($category),
156 'attributes' => array(
157 'rel' => 'tag',
158 'title' => check_plain(strip_tags($category->description)),
159 ),
160 );
161 }
162 }
163 // Previewing free tagging categories; we don't link them because the
164 // category-page might not exist yet.
165 else {
166 foreach ($category as $free_typed) {
167 $typed_categories = drupal_explode_tags($free_typed);
168 $types_categories_count = 0;
169 foreach ($typed_categories as $typed_category) {
170 $links['category_preview_cat_'. $types_categories_count] = array(
171 'title' => $typed_category,
172 );
173 $types_categories_count++;
174 }
175 }
176 }
177 }
178 }
179
180 // We call this hook again because some modules and themes
181 // call category_link('categories') directly.
182 drupal_alter('link', $links, $node);
183
184 return $links;
185 }
186 }
187
188 /**
189 * For containers not maintained by category.module, give the maintaining
190 * module a chance to provide a path for categories in that container.
191 *
192 * @param $category
193 * A category object.
194 * @return
195 * An internal Drupal path.
196 */
197
198 function category_category_path($category) {
199 if (function_exists('taxonomy_term_path')) {
200 $node = $category;
201 $node->category = (array) $category;
202 return taxonomy_term_path((object) _taxonomy_category_into_term($node));
203 }
204 return 'node/'. $category->cid;
205 }
206
207 /**
208 * Implementation of hook_menu().
209 */
210 function category_menu() {
211 $items = array();
212
213 $items['admin/content/category'] = array(
214 'title' => 'Categories',
215 'description' => 'Manage category hierarchies and classification of your content.',
216 'page callback' => 'drupal_get_form',
217 'page arguments' => array('category_overview_containers'),
218 'access arguments' => array('administer categories'),
219 'file' => 'category.admin.inc',
220 );
221 $items['admin/content/category/list'] = array(
222 'title' => 'List',
223 'type' => MENU_DEFAULT_LOCAL_TASK,
224 'weight' => -10,
225 );
226 $items['admin/content/category/add'] = array(
227 'title' => 'Add container',
228 'page callback' => 'category_add_container_page',
229 'access arguments' => array('administer categories'),
230 'type' => MENU_LOCAL_TASK,
231 'weight' => -9,
232 'file' => 'category.admin.inc',
233 );
234 $items['admin/content/category/wrappers'] = array(
235 'title' => 'Wrapper modules',
236 'page callback' => 'drupal_get_form',
237 'page arguments' => array('category_wrapper_admin_page'),
238 'access arguments' => array('administer categories'),
239 'type' => MENU_LOCAL_TASK,
240 'weight' => -8,
241 'file' => 'category.admin.inc',
242 );
243 $items['admin/content/category/%'] = array(
244 'title' => 'List categories',
245 'page callback' => 'drupal_get_form',
246 'page arguments' => array('category_overview_categories', 3),
247 'access arguments' => array('administer categories'),
248 'type' => MENU_CALLBACK,
249 'file' => 'category.admin.inc',
250 );
251
252 $items['admin/content/category/%/list'] = array(
253 'title' => 'List',
254 'type' => MENU_DEFAULT_LOCAL_TASK,
255 'weight' => -10,
256 );
257 $items['admin/content/category/%/add'] = array(
258 'title' => 'Add category',
259 'page callback' => 'category_add_category_page',
260 'page arguments' => array(3),
261 'access arguments' => array('administer categories'),
262 'type' => MENU_LOCAL_TASK,
263 'weight' => -9,
264 'file' => 'category.admin.inc',
265 );
266
267 $items['category/%'] = array(
268 'title' => 'Category listing',
269 'page callback' => 'category_page',
270 'page arguments' => array(1),
271 'access arguments' => array('access content'),
272 'type' => MENU_CALLBACK,
273 'file' => 'category.pages.inc',
274 );
275 $items['node/%/feed'] = array(
276 'title' => 'RSS feed',
277 'page callback' => 'category_feed',
278 'page arguments' => array(1),
279 'access arguments' => array('access content'),
280 'type' => MENU_CALLBACK,
281 'file' => 'category.pages.inc',
282 );
283
284 $items['category/wrapper'] = array(
285 'title' => 'Category wrapper',
286 'page callback' => 'category_wrapper',
287 'access arguments' => array('administer categories'),
288 'type' => MENU_CALLBACK,
289 'file' => 'category.pages.inc',
290 );
291 $items['category/autocomplete'] = array(
292 'title' => 'Autocomplete category',
293 'page callback' => 'category_autocomplete',
294 'access arguments' => array('access content'),
295 'type' => MENU_CALLBACK,
296 'file' => 'category.pages.inc',
297 );
298 $items['category/js/form'] = array(
299 'page callback' => 'category_form_update',
300 'access arguments' => array('access content'),
301 'type' => MENU_CALLBACK,
302 'file' => 'category.pages.inc',
303 );
304 $items['category/js/distant/%/%'] = array(
305 'page callback' => 'category_distant_update',
306 'page arguments' => array(3, 4),
307 'access arguments' => array('access content'),
308 'type' => MENU_CALLBACK,
309 'file' => 'category.pages.inc',
310 );
311
312 return $items;
313 }
314
315 /**
316 * Implementation of hook_init(). Add's the category module's CSS.
317 */
318 function category_init() {
319 drupal_add_css(drupal_get_path('module', 'category') .'/category.css');
320 require_once dirname(drupal_get_filename('module', 'category')) .'/category.inc';
321 }
322
323 /**
324 * Implementation of hook_form_alter().
325 *
326 * Adds the category or container fieldset to the node form.
327 *
328 * Also generates a form for selecting categories to associate with a node.
329 * We check for category_override_selector before loading the full
330 * category list, so contrib modules can intercept before hook_form_alter
331 * and provide scalable alternatives.
332 */
333 function category_form_alter(&$form, $form_state, $form_id) {
334 // Category node type behavior settings.
335 if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
336 $default_behavior = variable_get('category_behavior_'. $form['#node_type']->type, 0);
337 if ($default_behavior === 'container' || $default_behavior === 0) {
338 drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() { $('div.form-item:has(div.category-allowed-containers)').css('display', 'none'); }); }", 'inline');
339 }
340 drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() { $('input.category-behavior').click(function() {if ( this.value == 'category') { $('div.form-item:has(div.category-allowed-containers)').show('fast'); } else { $('div.form-item:has(div.category-allowed-containers)').hide('fast'); } }); }); }", 'inline');
341 $form['category'] = array(
342 '#type' => 'fieldset',
343 '#title' => t('Category settings'),
344 '#collapsible' => TRUE,
345 '#collapsed' => TRUE,
346 );
347 $behavior_disabled = FALSE;
348 $existing_nodes = 0;
349 if (!empty($default_behavior)) {
350 if ($existing_nodes = db_result(db_query("SELECT COUNT(c.cid) AS cid_count FROM {category} c INNER JOIN {node} n ON c.cid = n.nid WHERE n.type = '%s'", $form['#node_type']->type))) {
351 $behavior_disabled = TRUE;
352 }
353 }
354 $behavior_options = array(
355 0 => t('None'),
356 'category' => t('Category'),
357 'container' => t('Container'),
358 );
359 $count_message = format_plural($existing_nodes, t(' <strong>Note:</strong> this node type is already a @type, and there is %count node of this type that has corresponding @type information. You cannot change the behavior of this node type unless you delete the existing node.', array('@type' => drupal_strtolower($behavior_options[$default_behavior]), '%count' => $existing_nodes)), t(' <strong>Note:</strong> this node type is already a @type, and there are %count nodes of this type that have corresponding @type information. You cannot change the behavior of this node type unless you delete all of these nodes.', array('@type' => drupal_strtolower($behavior_options[$default_behavior]), '%count' => $existing_nodes)));
360 $form['category']['category_behavior'] = array(
361 '#type' => 'radios',
362 '#title' => t('Behavior'),
363 '#default_value' => $default_behavior,
364 '#required' => TRUE,
365 '#options' => $behavior_options,
366 '#description' => t('Attaches category or container behavior to this node type.') . ($behavior_disabled ? $count_message : ''),
367 '#attributes' => array('class' => 'category-behavior'),
368 '#disabled' => $behavior_disabled,
369 );
370 $form['category']['category_allowed_containers'] = array(
371 '#type' => 'checkboxes',
372 '#title' => t('Allowed containers'),
373 '#default_value' => variable_get('category_allowed_containers_'. $form['#node_type']->type, array()),
374 '#options' => _category_allowed_containers_options(),
375 '#description' => t('Applies only if this content type has its behavior set to \'category\'. If so, this specifies the containers that categories of this content type may belong to. Leave all options un-checked to allow all containers.'),
376 '#attributes' => array('class' => 'category-allowed-containers'),
377 );
378 }
379
380 // Category / container node add / edit form elements.
381 if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
382 // Add elements to the node form
383 $node = $form['#node'];
384
385 if (isset($node->category)) {
386 $form['#submit'][] = 'category_node_form_submit';
387 _category_add_form_elements($form, $node);
388 $form['category']['hierarchy']['pick-container'] = array(
389 '#type' => 'submit',
390 '#value' => t('Change container (update list of parents)'),
391 // Submit the node form so the parent select options get updated.
392 // This is typically only used when JS is disabled. Since the parent options
393 // won't be changed via AJAX, a button is provided in the node form to submit
394 // the form and generate options in the parent select corresponding to the
395 // selected book. This is similar to what happens during a node preview.
396 '#submit' => array('node_form_submit_build_node'),
397 '#weight' => 5,
398 );
399 }
400 }
401
402 // Category selection form elements.
403 if (isset($form['type']) && isset($form['#node']) && (!variable_get('category_override_selector', FALSE)) && $form['type']['#value'] .'_node_form' == $form_id) {
404 $node = $form['#node'];
405
406 if (!isset($node->categories)) {
407 $categories = array();
408 if (!empty($node->nid)) {
409 $categories = category_node_get_categories($node);
410 }
411 else if (arg(0) == 'node' && arg(1) == 'add' && is_numeric(arg(3))) {
412 $categories[arg(3)] = category_get_category(arg(3));
413 }
414 }
415 else {
416 // After preview the categories must be converted to objects.
417 if (isset($form_state['node_preview'])) {
418 $node->categories = category_preview_categories($node);
419 }
420 $categories = $node->categories;
421 }
422
423 $c = db_query(db_rewrite_sql("SELECT n.nid, n.title, cn.*, c.weight FROM {category_cont} cn INNER JOIN {category_cont_node_types} nt ON cn.cid = nt.cid INNER JOIN {category} c ON cn.cid = c.cid INNER JOIN {node} n ON cn.cid = n.nid WHERE nt.type = '%s' ORDER BY c.weight, n.title", 'n', 'nid'), $node->type);
424 $containers = array();
425 $container_map = array();
426 $weight_offset = 0;
427
428 // Build list of non-free-tagging containers, all of which are
429 // candidates for AJAX distant-parent behavior.
430 while ($container = db_fetch_object($c)) {
431 $containers[] = $container;
432 end($containers);
433 $container_map[$container->cid] = key($containers);
434 }
435
436 foreach ($containers as $key => $container) {
437 if ($container->tags) {
438 if (isset($form_state['node_preview'])) {
439 // Typed string can be changed by the user before preview,
440 // so we just insert the tags directly as provided in the form.
441 $typed_string = $node->categories['tags'][$container->cid];
442 }
443 else {
444 $typed_string = category_implode_tags($categories, $container->cid) . (array_key_exists('tags', $categories) ? $categories['tags'][$container->cid] : NULL);
445 }
446 if ($container->help) {
447 $help = $container->help;
448 }
449 else {
450 $help = t('A comma-separated list of categories describing this content. Example: funny, bungee jumping, "Company, Inc.".');
451 }
452 $form['categories']['tags'][$container->cid] = array(
453 '#type' => 'textfield',
454 '#title' => $container->title,
455 '#description' => $help,
456 '#required' => $container->required,
457 '#default_value' => $typed_string,
458 '#autocomplete_path' => 'category/autocomplete/'. $container->cid,
459 '#weight' => $container->weight,
460 '#maxlength' => 255,
461 );
462 }
463 else {
464 // Extract categories belonging to the container in question.
465 $default_categories = array();
466 foreach ($categories as $category) {
467 // Free tagging has no default categories and also no container id after preview.
468 if (isset($category->cnid) && $category->cnid == $container->cid) {
469 $default_categories[$category->cid] = $category;
470 }
471 }
472 if (empty($default_categories)) {
473 $default_categories = array(0);
474 }
475
476 $parents = NULL;
477 // Find the default categories for the parent of this container -
478 // needed only for AJAX distant parent selection.
479 if (!empty($container->allowed_parent)) {
480 if (isset($containers[$container_map[$container->allowed_parent]])) {
481 $parent_cnid = $containers[$container_map[$container->allowed_parent]]->cid;
482 $parents_array = array();
483 foreach ($categories as $category) {
484 if (isset($category->cnid) && $category->cnid == $parent_cnid) {
485 $parents_array[] = $category->cid;
486 }
487 }
488
489 $parents = $parents_array;
490 }
491 }
492
493 $form['categories'][$container->cid] = category_form($container->cid, array_keys($default_categories), $container->help, 'category', $parents);
494 // Weight offset is used to leave 'extra room' between select elements,
495 // so that submit buttons can be added where necessary (for AJAX
496 // distant parent containers).
497 if ($weight_offset) {
498 $weight_offset *= 2;
499 }
500 $form['categories'][$container->cid]['#weight'] = (!empty($container->weight) ? $container->weight : 0) + $weight_offset;
501 if (!$weight_offset) {
502 $weight_offset = 1;
503 }
504 $form['categories'][$container->cid]['#required'] = TRUE;
505 }
506 }
507 if (!empty($form['categories']) && is_array($form['categories'])) {
508 if (count($form['categories']) > 1) {
509 // Add fieldset only if form has more than 1 element.
510 $form['categories'] += array(
511 '#type' => 'fieldset',
512 '#title' => t('Categories'),
513 '#collapsible' => TRUE,
514 '#collapsed' => FALSE,
515 );
516 }
517 $form['categories']['#weight'] = -3;
518 $form['categories']['#tree'] = TRUE;
519 }
520
521 for ($curr = 0; $curr < count($containers); $curr++) {
522 for ($cand = 0; $cand < count($containers); $cand++) {
523 if (!empty($containers[$cand]->allowed_parent) && $containers[$cand]->allowed_parent == $containers[$curr]->cid) {
524 // For any pair of container select lists on this form where one
525 // container has another as its distant parent, add an AHAH callback
526 // to the parent, and add a wrapper div to the child.
527 // NOTE: this will fail in cases where multiple containers share a
528 // single distant parent. In such cases, the AHAH behavior will only
529 // apply to the first child container. This is because Drupal's AHAH
530 // framework does not support affecting multiple elements in a single
531 // callback.
532 if (!isset($form['categories'][$containers[$curr]->cid]['#ahah'])) {
533 $form['categories'][$containers[$curr]->cid] += array(
534 '#ahah' => array(
535 'path' => 'category/js/distant/'. $containers[$curr]->cid .'/'. $containers[$cand]->cid,
536 'wrapper' => 'edit-category-'. $containers[$cand]->cid .'-wrapper',
537 'effect' => 'slide',
538 ),
539 );
540
541 $form['categories'][$containers[$cand]->cid] += array(
542 '#prefix' => '<div id="edit-category-'. $containers[$cand]->cid .'-wrapper" class="category-distant-wrapper">',
543 '#suffix' => '</div>',
544 );
545
546 $form['categories'][$containers[$curr]->cid .'-pick'] = array(
547 '#type' => 'submit',
548 '#value' => t('Update children of @parent', array(
549 '@parent' => $containers[$curr]->title)
550 ),
551 // Submit the node form so the child select options get updated.
552 // This is typically only used when JS is disabled. Since the child options
553 // won't be changed via AJAX, a button is provided in the node form to submit
554 // the form and generate options in the child select corresponding to the
555 // appropriate container. This is similar to what happens during a node preview.
556 '#submit' => array('node_form_submit_build_node'),
557 '#weight' => $form['categories'][$containers[$curr]->cid]['#weight'] + 1,
558 );
559
560 // Need this for AJAX.
561 if (!isset($form['categories']['#cache'])) {
562 $form['categories']['#cache'] = TRUE;
563 }
564 drupal_add_js("if (Drupal.jsEnabled) { $(document).ready(function() { $('#edit-categories-". $containers[$curr]->cid ."-pick').css('display', 'none'); }); }", 'inline');
565 }
566 }
567 }
568 }
569 }
570 }
571
572 /**
573 * Additional form submit handler for category/container node forms.
574 *
575 * Flattens and processes the $form_state['values']['category'] array
576 * on both submits and previews. This is needed because
577 * the 'category' fieldset on the node form has to be 'tree = TRUE', but
578 * we don't want to deal with the fieldset hierarchy all the time.
579 */
580 function category_node_form_submit($form, &$form_state) {
581 $fieldsets = array(
582 'hierarchy' => TRUE,
583 'advanced' => TRUE,
584 'identification' => TRUE,
585 'content_types' => TRUE,
586 'tagging' => TRUE,
587 'distant' => TRUE,
588 );
589 foreach ($form_state['values']['category'] as $key => $value) {
590 if (isset($fieldsets[$key]) && is_array($value)) {
591 $form_state['values']['category'] += $value;
592 unset($form_state['values']['category'][$key]);
593 }
594 }
595
596 if (!empty($form_state['values']['category']['cid']) && $form_state['values']['category']['cid'] == 'new') {
597 unset($form_state['values']['category']['cid']);
598 }
599
600 // Load the new parent for distant-parent category selection.
601 if (!empty($form_state['values']['op'])) {
602 $containers = category_get_containers($node->type);
603 $is_valid_submission = FALSE;
604 foreach ($containers as $container) {
605 if ($form_state['values']['op'] == t('Update children of @parent', array('@parent' => $container->title))) {
606 $is_valid_submission = TRUE;
607 }
608 }
609
610 if ($is_valid_submission) {
611 foreach ($containers as $container) {
612 if (!empty($form_state['values']['categories'][$container->cid])) {
613 $post_categories = $form_state['values']['categories'][$container->cid];
614 if (!is_array($post_categories)) {
615 if (is_numeric($post_categories)) {
616 $post_categories = (int) $post_categories;
617 $node->categories[$post_categories] = category_get_category($post_categories);
618 }
619 }
620 else {
621 foreach ($post_categories as $post_category) {
622 if (is_numeric($post_category)) {
623 $post_category = (int) $post_category;
624 $node->categories[$post_category] = category_get_category($post_category);
625 }
626 }
627 }
628 }
629 }
630 }
631 }
632 }
633
634 /**
635 * Generate a form element for selecting categories from a container.
636 */
637 function category_form($cnid, $value = 0, $help = NULL, $name = 'category',
638 $parents = NULL) {
639 $container = category_get_container($cnid);
640 $help = ($help) ? $help : $container->help;
641
642 if (!$container->multiple) {
643 $blank = ($container->required) ? t('- Please choose -') : t('- None selected -');
644 }
645 else {
646 $blank = ($container->required) ? 0 : t('- None -');
647 }
648
649 return _category_category_select(check_plain($container->title), $name, $value, $cnid, $help, intval($container->multiple), $blank, array(), $parents);
650 }
651
652 /**
653 * Helper function to convert categories after a preview.
654 *
655 * After preview the tags are an array instead of proper objects. This function
656 * converts them back to objects with the exception of 'free tagging' categories,
657 * because new tags can be added by the user before preview and those do not
658 * yet exist in the database. We therefore save those tags as a string so
659 * we can fill the form again after the preview.
660 */
661 function category_preview_categories($node) {
662 $categories = array();
663 if (isset($node->categories)) {
664 foreach ($node->categories as $key => $category) {
665 unset($node->categories[$key]);
666 // A 'Multiple select' and a 'Free tagging' field returns an array.
667 if (is_array($category)) {
668 foreach ($category as $cid) {
669 if ($key == 'tags') {
670 // Free tagging; the values will be saved for later as strings
671 // instead of objects to fill the form again.
672 $categories['tags'] = $category;
673 }
674 else {
675 $categories[$cid] = category_get_category($cid);
676 }
677 }
678 }
679 // A 'Single select' field returns the category id.
680 elseif ($category) {
681 $categories[$category] = category_get_category($category);
682 }
683 }
684 }
685 return $categories;
686 }
687
688 /**
689 * Provides category information for rss feeds.
690 */
691 function category_rss_item($node) {
692 $output = array();
693 foreach ($node->categories as $cat) {
694 $is_included = TRUE;
695 if (module_exists('category_display')) {
696 $cont = category_display_get_container($cat->cnid);
697 if (!$cont->nodelinks) {
698 $is_included = FALSE;
699 }
700 }
701
702 if ($is_included) {
703 $output[] = array(
704 'key' => 'category',
705 'value' => check_plain($cat->title),
706 'attributes' => array(
707 'domain' => url('node/'. $cat->cid, array('absolute' => TRUE))
708 ),
709 );
710 }
711 }
712 return $output;
713 }
714
715 /**
716 * Make sure incoming cnids are free tagging enabled.
717 */
718 function category_node_validate(&$node) {
719 if (!empty($node->categories)) {
720 $categories = $node->categories;
721 if (!empty($categories['tags'])) {
722 foreach ($categories['tags'] as $cnid => $cnid_value) {
723 $container = category_get_container($cnid);
724 if (empty($container->tags)) {
725 // see form_get_error $key = implode('][', $element['#parents']);
726 // on why this is the key
727 form_set_error("categories][tags][$cnid", t('The %title container can not be modified in this way.', array('%title' => $container->title)));
728 }
729 }
730 }
731 else {
732 foreach ($categories as $cnid => $category) {
733 if (empty($category)) {
734 $container = category_get_container($cnid);
735 if ($container->required) {
736 form_set_error("categories][$cnid", t('You must choose a category from %container.', array('%container' => $container->title)));
737 }
738 }
739 }
740 }
741 }
742 }
743
744 /**
745 * Save category associations for a given node.
746 */
747 function category_node_save($node, $categories) {
748 global $user;
749
750 category_node_delete_revision($node);
751
752 // Free tagging containers do not send their cids in the form,
753 // so we'll detect them here and process them independently.
754 if (isset($categories['tags'])) {
755 $typed_input = $categories['tags'];
756 unset($categories['tags']);
757
758 foreach ($typed_input as $cnid => $cnid_value) {
759 $typed_cats = drupal_explode_tags($cnid_value);
760
761 $inserted = array();
762 foreach ($typed_cats as $typed_cat) {
763 // See if the term exists in the chosen container
764 // and return the cid, otherwise, add a new record.
765 $possibilities = category_get_category_by_name($typed_cat);
766 $typed_cat_cid = NULL; // cid match if any.
767 foreach ($possibilities as $possibility) {
768 if ($possibility->cnid == $cnid) {
769 $typed_cat_cid = $possibility->cid;
770 }
771 }
772
773 if (!$typed_cat_cid) {
774 $tag_node = new stdClass();
775 $tag_node->title = $typed_cat;
776 $tag_node->type = 'category';
777 $tag_node->category['cnid'] = $cnid;
778 $tag_node->category['parents'][0] = $cnid;
779 $node_options = variable_get('node_options_'. $tag_node->type, array('status', 'promote'));
780 $tag_node->status = in_array('status', $node_options);
781 $tag_node->promote = in_array('promote', $node_options);
782 $tag_node->sticky = in_array('sticky', $node_options);
783 $tag_node->revision = in_array('revision', $node_options);
784 $tag_node->name = $user->name ? $user->name : 0;
785 $tag_node->date = date('j M Y H:i:s');
786 $tag_node = node_submit($tag_node);
787 node_save($tag_node);
788 $typed_cat_cid = $tag_node->nid;
789 }
790
791 // Defend against duplicate, differently cased tags
792 if (!isset($inserted[$typed_cat_cid])) {
793 db_query('INSERT INTO {category_node} (nid, vid, cid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $typed_cat_cid);
794 $inserted[$typed_cat_cid] = TRUE;
795 }
796 }
797 }
798 }
799
800 if (is_array($categories)) {
801 foreach ($categories as $key => $cat) {
802 if ($key != 'tags') {
803 if (is_array($cat)) {
804 foreach ($cat as $cid) {
805 if (!empty($cid)) {
806 db_query('INSERT INTO {category_node} (nid, vid, cid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $cid);
807 }
808 }
809 }
810 else if (isset($cat->cid)) {
811 db_query('INSERT INTO {category_node} (nid, vid, cid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $cat->cid);
812 }
813 else if ($cat) {
814 db_query('INSERT INTO {category_node} (nid, vid, cid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $cat);
815 }
816 }
817 }
818 }
819 }
820
821 /**
822 * Remove associations of a node to its categories.
823 */
824 function category_node_delete($node) {
825 db_query('DELETE FROM {category_node} WHERE nid = %d', $node->nid);
826 category_cache_op('flush', $node->nid);
827 }
828
829 /**
830 * Remove associations of a node to its terms.
831 */
832 function category_node_delete_revision($node) {
833 db_query('DELETE FROM {category_node} WHERE vid = %d', $node->vid);
834 category_cache_op('flush', $node->nid);
835 }
836
837 /**
838 * Implementation of hook_node_type().
839 */
840 function category_node_type($op, $info) {
841 if ($op == 'update' && !empty($info->old_type) && $info->type != $info->old_type) {
842 db_query("UPDATE {category_cont_node_types} SET type = '%s' WHERE type = '%s'", $info->type, $info->old_type);
843
844 $allowed_containers = variable_get('category_allowed_containers_'. $info->old_type, array());
845 variable_del('category_allowed_containers_'. $info->old_type);
846 if (!empty($allowed_containers)) {
847 variable_set('category_allowed_containers_'. $info->type, $allowed_containers);
848 }
849
850 $behavior = variable_get('category_behavior_'. $info->old_type, 0);
851 variable_del('category_behavior_'. $info->old_type);
852 if (!empty($behavior)) {
853 variable_set('category_behavior_'. $info->type, $behavior);
854 }
855 }
856 elseif ($op == 'delete') {
857 db_query("DELETE FROM {category_cont_node_types} WHERE type = '%s'", $info->type);
858 variable_del('category_allowed_containers_'. $info->type);
859 variable_del('category_behavior_'. $info->type);
860 }
861 }
862
863 /**
864 * Implementation of hook_nodeapi().
865 *
866 * Handles category loading, inserting and updating.
867 */
868 function category_nodeapi(&$node, $op, $teaser, $page) {
869 switch ($op) {
870 case 'load':
871 $output = array();
872 $behavior = variable_get('category_behavior_'. $node->type, 0);
873 if (!empty($behavior)) {
874 if ($behavior == 'category') {
875 $output['category'] = (array) category_get_category($node->nid);
876 }
877 else {
878 $output['category'] = (array) category_get_container($node->nid);
879 }
880 $output['category']['parents'] = category_get_parents($node->nid);
881 $output['category']['behavior'] = $behavior;
882 }
883 $output['categories'] = category_node_get_categories($node);
884 return $output;
885
886 case 'view':
887 if (!$teaser) {
888 $behavior = variable_get('category_behavior_'. $node->type, 0);
889 if (isset($behavior) && $node->build_mode == NODE_BUILD_NORMAL && ($listing = category_node_listing($node))) {
890 $node->content['category_listing'] = array(
891 '#value' => $listing,
892 '#weight' => 100,
893 );
894 }
895 }
896 break;
897
898 case 'prepare':
899 $behavior = variable_get('category_behavior_'. $node->type, 0);
900 if (!empty($behavior)) {
901 // Prepare defaults for the add/edit form.
902 if (empty($node->category)) {
903 $node->category = array();
904 if (empty($node->nid) && isset($_GET['parent']) &&
905 is_numeric($_GET['parent'])) {
906 // Handle "Add child page" links:
907 $parent = category_get_category($_GET['parent']);
908 if (!empty($parent)) {
909 $cnid = !empty($parent->cnid) ? $parent->cnid : $parent->cid;
910 $parent_container = category_get_container($cnid);
911 $allowed_containers = variable_get('category_allowed_containers_'. $node->type, array());
912 if (empty($parent->category['allowed_parent'])) {
913 $node->category['container'] = $cnid;
914 }
915 else {
916 $allowed_key = array_search(
917 $parent->category['allowed_parent'], $allowed_containers);
918 $node->category['container'] = ($allowed_key !== FALSE) ? $allowed_containers[$allowed_key] : $cnid;
919 }
920 $node->category['parents'] = array($parent->nid => $parent->nid);
921 }
922 }
923 if (isset($node->category['container']) && !isset($node->category['original_container'])) {
924 $node->category['original_container'] = $node->category['container'];
925 }
926 // Set defaults.
927 $node->category += _category_defaults(!empty($node->nid) ? $node->nid : 'new');
928 }
929 else {
930 $node->category['container'] = empty($node->category['container']) ? $node->category['cnid'] : $node->category['container'];
931 if (empty($node->category['container']) && !empty($node->category['parents']) && $behavior == 'container') {
932 reset($node->category['parents']);
933 $first_parent = current($node->category['parents']);
934 $node->category['container'] = (!empty($first_parent->cnid) ? $first_parent->cnid : $first_parent->cid);
935 }
936 if (isset($node->category['container']) && !isset($node->category['original_container'])) {
937 $node->category['original_container'] = $node->category['container'];
938 }
939 }
940 // Find the depth limit for the parent select.
941 if (isset($node->category['container']) && !isset($node->category['parent_depth_limit'])) {
942 $node->category['parent_depth_limit'] = (MENU_MAX_DEPTH - 1);
943 }
944 $node->category['behavior'] = $behavior;
945 }
946 break;
947
948 case 'insert':
949 case 'update':
950 $behavior = variable_get('category_behavior_'. $node->type, 0);
951 if (!empty($behavior)) {
952 if ($behavior == 'category') {
953 category_save_category($node);
954 }
955 else {
956 category_save_container($node);
957 }
958 }
959
960 if (!empty($node->categories)) {
961 category_node_save($node, _category_filter_pick_elements($node->categories));
962 }
963 break;
964
965 case 'delete':
966 $behavior = variable_get('category_behavior_'. $node->type, 0);
967 if (!empty($behavior)) {
968 if ($behavior == 'category') {
969 category_del_category($node->nid);
970 }
971 else {
972 category_del_container($node->nid);
973 }
974 }
975
976 category_node_delete($node);
977 break;
978
979 case 'delete revision':
980 category_node_delete_revision($node);
981 break;
982
983 case 'validate':
984 category_node_validate($node);
985 break;
986
987 case 'rss item':
988 return category_rss_item($node);
989
990 case 'update index':
991 return category_node_update_index($node);
992 }
993 }
994
995 /**
996 * Implementation of hook_nodeapi('update_index').
997 */
998 function category_node_update_index(&$node) {
999 $output = array();
1000 if (!empty($node->category)) {
1001 foreach ($node->category as $cat) {
1002 $output[] = $cat->title;
1003 }
1004 }
1005 if (count($output)) {
1006 return '<strong>('. implode(', ', $output) .')</strong>';
1007 }
1008 }
1009
1010 /**
1011 * A recursive helper function for category_toc().
1012 */
1013 function _category_toc_recurse($tree, $indent, &$toc, $exclude, $depth_limit) {
1014 foreach ($tree as $data) {
1015 if (($data->depth + 1) > $depth_limit) {
1016 // Don't iterate through any links on this level.
1017 break;
1018 }
1019 if (!in_array($data->cid, $exclude)) {
1020 $toc[$data->cid] = str_repeat($indent, $data->depth + 1) .' '. truncate_utf8($data->title, 30, TRUE, TRUE);
1021 }
1022 }
1023 }
1024
1025 /**
1026 * Returns an array of container children in table of contents order.
1027 *
1028 * @param $container
1029 * The ID of the container whose children are to be listed.
1030 * @param $exclude
1031 * Optional array of nid values. Any category whose nid is in this array
1032 * will be excluded (along with its children).
1033 * @param $depth_limit
1034 * Any category deeper than this value will be excluded (along with its children).
1035 * @return
1036 * An array of nid, title pairs for use as options for selecting a category.
1037 */
1038 function category_toc($container, $exclude = array(), $depth_limit) {
1039 $tree = category_get_tree($container);
1040 $container = category_get_container($container);
1041 if (!in_array($container->cid, $exclude)) {
1042 $toc[$container->cid] = $container->title;
1043 }
1044 _category_toc_recurse($tree, '--', $toc, $exclude, $depth_limit);
1045
1046 return $toc;
1047 }
1048
1049 /**
1050 * Saves a container to the database. This function is called when node_save()
1051 * is run on a node that has container behavior.
1052 */
1053 function category_save_container(&$node) {
1054 $new = empty($node->category['cid']);
1055 $node->category['cid'] = $node->nid;
1056 $node->category['cnid'] = 0;
1057 $node->category['nodes'] = empty($node->category['nodes']) ? array() : $node->category['nodes'];
1058
1059 if (!isset($node->category['module'])) {
1060 $node->category['module'] = 'category';
1061 }
1062 if (!empty($node->category['parents']) && !is_array($node->category['parents'])) {
1063 $node->category['parents'] = array($node->category['parents'] => TRUE);
1064 }
1065 if (empty($node->category['parents'])) {
1066 $node->category['parents'] = array(0 => TRUE);
1067 }
1068
1069 if ($new) {
1070 // Insert new.
1071 drupal_write_record('category', $node->category);
1072 drupal_write_record('category_cont', $node->category);
1073 $status = SAVED_NEW;
1074 }
1075 else {
1076 drupal_write_record('category', $node->category, 'cid');
1077 drupal_write_record('category_cont', $node->category,