diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/blog/blog.module ./modules/blog/blog.module --- ../C-HEAD/modules/blog/blog.module 2006-07-31 13:14:28.000000000 -0700 +++ ./modules/blog/blog.module 2006-07-31 23:44:09.000000000 -0700 @@ -10,7 +10,13 @@ * Implementation of hook_node_info(). */ function blog_node_info() { - return array('blog' => array('name' => t('blog entry'), 'base' => 'blog')); + return array( + 'blog' => array( + 'name' => t('blog entry'), + 'module' => 'blog', + 'description' => t('A blog is a regularly updated journal or diary made up of individual posts shown in reversed chronological order. Each member of the site may create and maintain a blog.'), + ) + ); } /** @@ -72,8 +78,6 @@ function blog_help($section) { return $output; case 'admin/settings/modules#description': return t('Enables keeping an easily and regularly updated web page or a blog.'); - case 'node/add#blog': - return t("A blog is a regularly updated journal or diary made up of individual posts shown in reversed chronological order. Each member of the site may create and maintain a blog."); } } @@ -201,6 +205,7 @@ function blog_page_last() { function blog_form(&$node) { global $nid; $iid = $_GET['iid']; + $type = node_get_types('type', $node); if (empty($node->body)) { @@ -221,8 +226,8 @@ function blog_form(&$node) { } - $form['title'] = array('#type' => 'textfield', '#title' => t('Title'), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5); - $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE); + $form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5); + $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => check_plain($type->body_label), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE); $form['body_filter']['filter'] = filter_form($node->format); return $form; } diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/book/book.module ./modules/book/book.module --- ../C-HEAD/modules/book/book.module 2006-07-31 13:14:28.000000000 -0700 +++ ./modules/book/book.module 2006-08-01 00:15:32.000000000 -0700 @@ -10,7 +10,13 @@ * Implementation of hook_node_info(). */ function book_node_info() { - return array('book' => array('name' => t('book page'), 'base' => 'book')); + return array( + 'book' => array( + 'name' => t('book page'), + 'module' => 'book', + 'description' => t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written. So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it."), + ) + ); } /** @@ -84,10 +90,6 @@ function book_menu($may_cache) { if ($may_cache) { $items[] = array( - 'path' => 'node/add/book', - 'title' => t('book page'), - 'access' => user_access('create book pages')); - $items[] = array( 'path' => 'admin/content/book', 'title' => t('books'), 'description' => t('Manage site\'s books and orphaned book pages.'), @@ -224,6 +226,7 @@ function book_submit(&$node) { * Implementation of hook_form(). */ function book_form(&$node) { + $type = node_get_types('type', $node); if ($node->nid && !$node->parent && !user_access('create new books')) { $form['parent'] = array('#type' => 'value', '#value' => $node->parent); } @@ -238,13 +241,13 @@ function book_form(&$node) { } $form['title'] = array('#type' => 'textfield', - '#title' => t('Title'), + '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -5, ); $form['body_filter']['body'] = array('#type' => 'textarea', - '#title' => t('Body'), + '#title' => check_plain($type->body_label), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE, @@ -1013,8 +1016,6 @@ function book_help($section) { return t('

The book module offers a means to organize content, authored by many users, in an online manual, outline or FAQ.

'); case 'admin/content/book/orphan': return t('

Pages in a book are like a tree. As pages are edited, reorganized and removed, child pages might be left with no link to the rest of the book. Such pages are referred to as "orphan pages". On this page, administrators can review their books for orphans and reattach those pages as desired.

'); - case 'node/add#book': - return t("A book is a collaborative writing effort: users can collaborate writing the pages of the book, positioning the pages in the right order, and reviewing or modifying pages previously written. So when you have some information to share or when you read a page of the book and you didn't like it, or if you think a certain page could have been written better, you can do something about it."); } if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'outline') { diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/comment/comment.module ./modules/comment/comment.module --- ../C-HEAD/modules/comment/comment.module 2006-07-31 13:14:28.000000000 -0700 +++ ./modules/comment/comment.module 2006-07-31 23:44:33.000000000 -0700 @@ -265,10 +265,16 @@ function comment_link($type, $node = NUL } function comment_form_alter($form_id, &$form) { - if (isset($form['type'])) { - if ($form['type']['#value'] .'_node_settings' == $form_id) { - $form['workflow']['comment_'. $form['type']['#value']] = array('#type' => 'radios', '#title' => t('Default comment setting'), '#default_value' => variable_get('comment_'. $form['type']['#value'], COMMENT_NODE_READ_WRITE), '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')), '#description' => t('Users with the administer comments permission will be able to override this setting.')); - } + if ($form_id == 'node_type_form' && isset($form['identity']['type'])) { + $form['workflow']['comment'] = array( + '#type' => 'radios', + '#title' => t('Default comment setting'), + '#default_value' => variable_get('comment_'. $form['identity']['type']['#default_value'], COMMENT_NODE_READ_WRITE), + '#options' => array(t('Disabled'), t('Read only'), t('Read/Write')), + '#description' => t('Users with the administer comments permission will be able to override this setting.'), + ); + } + elseif (isset($form['type'])) { if ($form['type']['#value'] .'_node_form' == $form_id) { $node = $form['#node']; if (user_access('administer comments')) { diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/forum/forum.module ./modules/forum/forum.module --- ../C-HEAD/modules/forum/forum.module 2006-07-31 13:14:28.000000000 -0700 +++ ./modules/forum/forum.module 2006-07-31 23:44:34.000000000 -0700 @@ -33,8 +33,6 @@ function forum_help($section) { return t('

Containers help you organize your forums. The job of a container is to hold, or contain, other forums that are related. For example, a container named "Food" might hold two forums named "Fruit" and "Vegetables".

'); case 'admin/content/forum/add/forum': return t('

A forum holds discussion topics that are related. For example, a forum named "Fruit" might contain topics titled "Apples" and "Bananas".

'); - case 'node/add#forum': - return t('Create a new topic for discussion in the forums.'); } } @@ -107,7 +105,14 @@ function forum_menu($may_cache) { * Implementation of hook_node_info(). */ function forum_node_info() { - return array('forum' => array('name' => t('forum topic'), 'base' => 'forum')); + return array( + 'forum' => array( + 'name' => t('forum topic'), + 'module' => 'forum', + 'description' => t('Create a new topic for discussion in the forums.'), + 'title_label' => t('Subject'), + ) + ); } /** @@ -379,7 +384,8 @@ function forum_update($node) { * Implementation of hook_form(). */ function forum_form(&$node) { - $form['title'] = array('#type' => 'textfield', '#title' => t('Subject'), '#default_value' => $node->title, '#required' => TRUE, '#weight' => -5); + $type = node_get_types('type', $node); + $form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#default_value' => $node->title, '#required' => TRUE, '#weight' => -5); if ($node->nid) { $forum_terms = taxonomy_node_get_terms_by_vocabulary(_forum_get_vid(), $node->nid); @@ -388,7 +394,7 @@ function forum_form(&$node) { $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.')); } - $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE); + $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => check_plain($type->body_label), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE); $form['body_filter']['format'] = filter_form($node->format); return $form; diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/node/.#node.module.1.664 ./modules/node/.#node.module.1.664 --- ../C-HEAD/modules/node/.#node.module.1.664 1969-12-31 16:00:00.000000000 -0800 +++ ./modules/node/.#node.module.1.664 2006-08-01 00:27:56.000000000 -0700 @@ -0,0 +1,2729 @@ +'. t('All content in a website is stored and treated as nodes. Therefore nodes are any postings such as blogs, stories, polls and forums. The node module manages these content types and is one of the strengths of Drupal over other content management systems.') .'

'; + $output .= '

'. t('Treating all content as nodes allows the flexibility of creating new types of content. It also allows you to painlessly apply new features or changes to all content. Comments are not stored as nodes but are always associated with a node.') .'

'; + $output .= t('

Node module features

+ +'); + $output .= t('

You can

+ +', array('%search' => url('search'), '%admin-settings-content-types' => url('admin/content/types'))); + $output .= '

'. t('For more information please read the configuration and customization handbook Node page.', array('%node' => 'http://drupal.org/handbook/modules/node/')) .'

'; + return $output; + case 'admin/settings/modules#description': + return t('Allows content to be submitted to the site and displayed on pages.'); + case 'admin/content/search': + return t('

Enter a simple pattern to search for a post. This can include the wildcard character *.
For example, a search for "br*" might return "bread bakers", "our daily bread" and "brenda".

'); + case 'admin/content/types': + return '

'. t('Below is a list of all the content types on your site. All posts that exist on your site are instances of one of these content types.') .'

'; + case 'admin/content/types/add': + return '

'. t('To create a new content type, enter the human-readable name, the machine-readable name, and all other relevant fields that are on this page. Once created, users of your site will be able to create posts that are instances of this content type.'); + } + + if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions') { + return t('The revisions let you track differences between multiple versions of a post.'); + } + + if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) { + $type = node_get_types('type', arg(2)); + return filter_xss_admin($type->help); + } +} + +/** + * Implementation of hook_cron(). + */ +function node_cron() { + db_query('DELETE FROM {history} WHERE timestamp < %d', NODE_NEW_LIMIT); +} + +/** + * Gather a listing of links to nodes. + * + * @param $result + * A DB result object from a query to fetch node objects. If your query joins the node_comment_statistics table so that the comment_count field is available, a title attribute will be added to show the number of comments. + * @param $title + * A heading for the resulting list. + * + * @return + * An HTML list suitable as content for a block. + */ +function node_title_list($result, $title = NULL) { + while ($node = db_fetch_object($result)) { + $items[] = l($node->title, 'node/'. $node->nid, $node->comment_count ? array('title' => format_plural($node->comment_count, '1 comment', '%count comments')) : ''); + } + + return theme('node_list', $items, $title); +} + +/** + * Format a listing of links to nodes. + */ +function theme_node_list($items, $title = NULL) { + return theme('item_list', $items, $title); +} + +/** + * Update the 'last viewed' timestamp of the specified node for current user. + */ +function node_tag_new($nid) { + global $user; + + if ($user->uid) { + if (node_last_viewed($nid)) { + db_query('UPDATE {history} SET timestamp = %d WHERE uid = %d AND nid = %d', time(), $user->uid, $nid); + } + else { + @db_query('INSERT INTO {history} (uid, nid, timestamp) VALUES (%d, %d, %d)', $user->uid, $nid, time()); + } + } +} + +/** + * Retrieves the timestamp at which the current user last viewed the + * specified node. + */ +function node_last_viewed($nid) { + global $user; + static $history; + + if (!isset($history[$nid])) { + $history[$nid] = db_fetch_object(db_query("SELECT timestamp FROM {history} WHERE uid = '$user->uid' AND nid = %d", $nid)); + } + + return (isset($history[$nid]->timestamp) ? $history[$nid]->timestamp : 0); +} + +/** + * Decide on the type of marker to be displayed for a given node. + * + * @param $nid + * Node ID whose history supplies the "last viewed" timestamp. + * @param $timestamp + * Time which is compared against node's "last viewed" timestamp. + * @return + * One of the MARK constants. + */ +function node_mark($nid, $timestamp) { + global $user; + static $cache; + + if (!$user->uid) { + return MARK_READ; + } + if (!isset($cache[$nid])) { + $cache[$nid] = node_last_viewed($nid); + } + if ($cache[$nid] == 0 && $timestamp > NODE_NEW_LIMIT) { + return MARK_NEW; + } + elseif ($timestamp > $cache[$nid] && $timestamp > NODE_NEW_LIMIT) { + return MARK_UPDATED; + } + return MARK_READ; +} + +/** + * Automatically generate a teaser for a node body in a given format. + */ +function node_teaser($body, $format = NULL) { + + $size = variable_get('teaser_length', 600); + + // find where the delimiter is in the body + $delimiter = strpos($body, ''); + + // If the size is zero, and there is no delimiter, the entire body is the teaser. + if ($size == 0 && $delimiter === FALSE) { + return $body; + } + + // We check for the presence of the PHP evaluator filter in the current + // format. If the body contains PHP code, we do not split it up to prevent + // parse errors. + if (isset($format)) { + $filters = filter_list_format($format); + if (isset($filters['filter/1']) && strpos($body, '' => 4, '
' => 0, '
' => 0, "\n" => 0, '. ' => 1, '! ' => 1, '? ' => 1, '。' => 3, '؟ ' => 1); + foreach ($breakpoints as $point => $charnum) { + if ($length = strpos($body, $point, $size)) { + return substr($body, 0, $length + $charnum); + } + } + + // If all else fails, we simply truncate the string. + return truncate_utf8($body, $size); +} + +/** + * Builds a list of available node types, and returns all of part of this list + * in the specified format. + * + * @param $op + * The format in which to return the list. When this is set to 'type', + * 'module', or 'name', only the specified node type is returned. When set to + * 'types' or 'names', all node types are returned. + * @param $node + * A node object, array, or string that indicates the node type to return. + * Leave at default value (NULL) to return a list of all node types. + * @param $reset + * Whether or not to reset this function's internal cache (defaults to + * FALSE). + * + * @return + * Either an array of all available node types, or a single node type, in a + * variable format. + */ +function node_get_types($op = 'types', $node = NULL, $reset = FALSE) { + static $_node_types, $_node_names; + + if ($reset || !isset($_node_types)) { + list($_node_types, $_node_names) = _node_types_build(); + } + + if ($node) { + if (is_array($node)) { + $type = $node['type']; + } + elseif (is_object($node)) { + $type = $node->type; + } + elseif (is_string($node)) { + $type = $node; + } + if (!isset($_node_types[$type])) { + return FALSE; + } + } + switch ($op) { + case 'types': + return $_node_types; + case 'type': + return $_node_types[$type]; + case 'module': + return $_node_types[$type]->module; + case 'names': + return $_node_names; + case 'name': + return $_node_names[$type]; + } +} + +/** + * Resets the database cache of node types, and saves all new or non-modified + * module-defined node types to the database. + */ +function node_types_rebuild() { + _node_types_build(); + + $node_types = node_get_types('types', NULL, TRUE); + + foreach ($node_types as $type => $info) { + if (!empty($info->is_new)) { + node_type_save($info); + } + } + + _node_types_build(); +} + +/** + * Saves a node type to the database. + * + * @param $info + * The node type to save, as an object. + * + * @return + * Status flag indicating outcome of the operation. + */ +function node_type_save($info) { + $is_existing = FALSE; + $existing_type = !empty($info->old_type) ? $info->old_type : $info->type; + $is_existing = db_num_rows(db_query("SELECT * FROM {node_type} WHERE type = '%s'", $existing_type)); + + if ($is_existing) { + db_query("UPDATE {node_type} SET type = '%s', name = '%s', module = '%s', has_title = %d, title_label = '%s', has_body = %d, body_label = '%s', description = '%s', help = '%s', min_word_count = %d, custom = %d, modified = %d, locked = %d WHERE type = '%s'", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $existing_type); + return SAVED_UPDATED; + } + else { + db_query("INSERT INTO {node_type} (type, name, module, has_title, title_label, has_body, body_label, description, help, min_word_count, custom, modified, locked, orig_type) VALUES ('%s', '%s', '%s', %d, '%s', %d, '%s', '%s', '%s', %d, %d, %d, %d, '%s')", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $info->orig_type); + return SAVED_NEW; + } +} + +/** + * Updates all nodes of one type to be of another type. + * + * @param $orig_type + * The current node type of the nodes. + * @param $type + * The new node type of the nodes. + * + * @return + * The number of nodes whose node type field was modified. + */ +function node_type_update_nodes($old_type, $type) { + return db_num_rows(db_query("UPDATE {node} SET type = '%s' WHERE type = '%s'", $type, $old_type)); +} + +/** + * Builds the list of available node types, by querying hook_node_info() in all + * modules, and by looking for node types in the database. + * + */ +function _node_types_build() { + $_node_types = array(); + $_node_names = array(); + + $info_array = module_invoke_all('node_info'); + foreach ($info_array as $type => $info) { + $info['type'] = $type; + $_node_types[$type] = (object) _node_type_set_defaults($info); + $_node_names[$type] = $info['name']; + } + + $type_result = db_query(db_rewrite_sql('SELECT nt.type, nt.* FROM {node_type} nt ORDER BY nt.type ASC', 'nt', 'type')); + while ($type_object = db_fetch_object($type_result)) { + if (!isset($_node_types[$type_object->type]) || $type_object->modified) { + $_node_types[$type_object->type] = $type_object; + $_node_names[$type_object->type] = $type_object->name; + + if ($type_object->type != $type_object->orig_type) { + unset($_node_types[$type_object->orig_type]); + unset($_node_names[$type_object->orig_type]); + } + } + } + + asort($_node_names); + + return array($_node_types, $_node_names); +} + +/** + * Set default values for a node type defined through hook_node_info(). + */ +function _node_type_set_defaults($info) { + if (!isset($info['has_title'])) { + $info['has_title'] = TRUE; + } + if ($info['has_title'] && !isset($info['title_label'])) { + $info['title_label'] = t('Title'); + } + + if (!isset($info['has_body'])) { + $info['has_body'] = TRUE; + } + if ($info['has_body'] && !isset($info['body_label'])) { + $info['body_label'] = t('Body'); + } + + if (!isset($info['custom'])) { + $info['custom'] = FALSE; + } + if (!isset($info['modified'])) { + $info['modified'] = FALSE; + } + if (!isset($info['locked'])) { + $info['locked'] = TRUE; + } + + $info['orig_type'] = $info['type']; + $info['is_new'] = TRUE; + + return $info; +} + +/** + * Determine whether a node hook exists. + * + * @param &$node + * Either a node object, node array, or a string containing the node type. + * @param $hook + * A string containing the name of the hook. + * @return + * TRUE iff the $hook exists in the node type of $node. + */ +function node_hook(&$node, $hook) { + $module = node_get_types('module', $node); + if ($module == 'node') { + $module = 'node_content'; // Avoid function name collisions. + } + return module_hook($module, $hook); +} + +/** + * Invoke a node hook. + * + * @param &$node + * Either a node object, node array, or a string containing the node type. + * @param $hook + * A string containing the name of the hook. + * @param $a2, $a3, $a4 + * Arguments to pass on to the hook, after the $node argument. + * @return + * The returned value of the invoked hook. + */ +function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) { + if (node_hook($node, $hook)) { + $module = node_get_types('module', $node); + if ($module == 'node') { + $module = 'node_content'; // Avoid function name collisions. + } + $function = $module .'_'. $hook; + return ($function($node, $a2, $a3, $a4)); + } +} + +/** + * Invoke a hook_nodeapi() operation in all modules. + * + * @param &$node + * A node object. + * @param $op + * A string containing the name of the nodeapi operation. + * @param $a3, $a4 + * Arguments to pass on to the hook, after the $node and $op arguments. + * @return + * The returned value of the invoked hooks. + */ +function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { + $return = array(); + foreach (module_implements('nodeapi') as $name) { + $function = $name .'_nodeapi'; + $result = $function($node, $op, $a3, $a4); + if (isset($result) && is_array($result)) { + $return = array_merge($return, $result); + } + else if (isset($result)) { + $return[] = $result; + } + } + return $return; +} + +/** + * Load a node object from the database. + * + * @param $param + * Either the nid of the node or an array of conditions to match against in the database query + * @param $revision + * Which numbered revision to load. Defaults to the current version. + * @param $reset + * Whether to reset the internal node_load cache. + * + * @return + * A fully-populated node object. + */ +function node_load($param = array(), $revision = NULL, $reset = NULL) { + static $nodes = array(); + + if ($reset) { + $nodes = array(); + } + + $arguments = array(); + if (is_numeric($param)) { + $cachable = $revision == NULL; + if ($cachable && isset($nodes[$param])) { + return $nodes[$param]; + } + $cond = 'n.nid = %d'; + $arguments[] = $param; + } + else { + // Turn the conditions into a query. + foreach ($param as $key => $value) { + $cond[] = 'n.'. db_escape_string($key) ." = '%s'"; + $arguments[] = $value; + } + $cond = implode(' AND ', $cond); + } + + // Retrieve the node. + // No db_rewrite_sql is applied so as to get complete indexing for search. + if ($revision) { + array_unshift($arguments, $revision); + $node = db_fetch_object(db_query('SELECT n.nid, r.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.nid = n.nid AND r.vid = %d WHERE '. $cond, $arguments)); + } + else { + $node = db_fetch_object(db_query('SELECT n.nid, n.vid, n.type, n.status, n.created, n.changed, n.comment, n.promote, n.sticky, r.timestamp AS revision_timestamp, r.title, r.body, r.teaser, r.log, r.format, u.uid, u.name, u.picture, u.data FROM {node} n INNER JOIN {users} u ON u.uid = n.uid INNER JOIN {node_revisions} r ON r.vid = n.vid WHERE '. $cond, $arguments)); + } + + if ($node->nid) { + // Call the node specific callback (if any) and piggy-back the + // results to the node or overwrite some values. + if ($extra = node_invoke($node, 'load')) { + foreach ($extra as $key => $value) { + $node->$key = $value; + } + } + + if ($extra = node_invoke_nodeapi($node, 'load')) { + foreach ($extra as $key => $value) { + $node->$key = $value; + } + } + } + + if ($cachable) { + $nodes[$param] = $node; + } + + return $node; +} + +/** + * Save a node object into the database. + */ +function node_save(&$node) { + global $user; + + $node->is_new = FALSE; + + // Apply filters to some default node fields: + if (empty($node->nid)) { + // Insert a new node. + $node->is_new = TRUE; + + $node->nid = db_next_id('{node}_nid'); + $node->vid = db_next_id('{node_revisions}_vid');; + } + else { + // We need to ensure that all node fields are filled. + $node_current = node_load($node->nid); + foreach ($node as $field => $data) { + $node_current->$field = $data; + } + $node = $node_current; + + if ($node->revision) { + $node->old_vid = $node->vid; + $node->vid = db_next_id('{node_revisions}_vid'); + } + } + + // Set some required fields: + if (empty($node->created)) { + $node->created = time(); + } + // The changed timestamp is always updated for bookkeeping purposes (revisions, searching, ...) + $node->changed = time(); + + // Split off revisions data to another structure + $revisions_table_values = array('nid' => $node->nid, 'vid' => $node->vid, + 'title' => $node->title, 'body' => $node->body, + 'teaser' => $node->teaser, 'log' => $node->log, 'timestamp' => $node->changed, + 'uid' => $user->uid, 'format' => $node->format); + $revisions_table_types = array('nid' => '%d', 'vid' => '%d', + 'title' => "'%s'", 'body' => "'%s'", + 'teaser' => "'%s'", 'log' => "'%s'", 'timestamp' => '%d', + 'uid' => '%d', 'format' => '%d'); + $node_table_values = array('nid' => $node->nid, 'vid' => $node->vid, + 'title' => $node->title, 'type' => $node->type, 'uid' => $node->uid, + 'status' => $node->status, 'created' => $node->created, + 'changed' => $node->changed, 'comment' => $node->comment, + 'promote' => $node->promote, 'sticky' => $node->sticky); + $node_table_types = array('nid' => '%d', 'vid' => '%d', + 'title' => "'%s'", 'type' => "'%s'", 'uid' => '%d', + 'status' => '%d', 'created' => '%d', + 'changed' => '%d', 'comment' => '%d', + 'promote' => '%d', 'sticky' => '%d'); + + //Generate the node table query and the + //the node_revisions table query + if ($node->is_new) { + $node_query = 'INSERT INTO {node} ('. implode(', ', array_keys($node_table_types)) .') VALUES ('. implode(', ', $node_table_types) .')'; + $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')'; + } + else { + $arr = array(); + foreach ($node_table_types as $key => $value) { + $arr[] = $key .' = '. $value; + } + $node_table_values[] = $node->nid; + $node_query = 'UPDATE {node} SET '. implode(', ', $arr) .' WHERE nid = %d'; + if ($node->revision) { + $revisions_query = 'INSERT INTO {node_revisions} ('. implode(', ', array_keys($revisions_table_types)) .') VALUES ('. implode(', ', $revisions_table_types) .')'; + } + else { + $arr = array(); + foreach ($revisions_table_types as $key => $value) { + $arr[] = $key .' = '. $value; + } + $revisions_table_values[] = $node->vid; + $revisions_query = 'UPDATE {node_revisions} SET '. implode(', ', $arr) .' WHERE vid = %d'; + } + } + + // Insert the node into the database: + db_query($node_query, $node_table_values); + db_query($revisions_query, $revisions_table_values); + + // Call the node specific callback (if any): + if ($node->is_new) { + node_invoke($node, 'insert'); + node_invoke_nodeapi($node, 'insert'); + } + else { + node_invoke($node, 'update'); + node_invoke_nodeapi($node, 'update'); + } + + // Clear the cache so an anonymous poster can see the node being added or updated. + cache_clear_all(); +} + +/** + * Generate a display of the given node. + * + * @param $node + * A node array or node object. + * @param $teaser + * Whether to display the teaser only, as on the main page. + * @param $page + * Whether the node is being displayed by itself as a page. + * @param $links + * Whether or not to display node links. Links are omitted for node previews. + * + * @return + * An HTML representation of the themed node. + */ +function node_view($node, $teaser = FALSE, $page = FALSE, $links = TRUE) { + $node = (object)$node; + + // Remove the delimiter (if any) that separates the teaser from the body. + // TODO: this strips legitimate uses of '' also. + $node->body = str_replace('', '', $node->body); + + if ($node->log != '' && !$teaser) { + $node->body .= '

'. t('Log') .':
'. filter_xss($node->log) .'
'; + } + + // The 'view' hook can be implemented to overwrite the default function + // to display nodes. + if (node_hook($node, 'view')) { + node_invoke($node, 'view', $teaser, $page); + } + else { + $node = node_prepare($node, $teaser); + } + // Allow modules to change $node->body before viewing. + node_invoke_nodeapi($node, 'view', $teaser, $page); + if ($links) { + $node->links = module_invoke_all('link', 'node', $node, !$page); + + foreach (module_implements('link_alter') AS $module) { + $function = $module .'_link_alter'; + $function($node, $node->links); + } + } + // unset unused $node part so that a bad theme can not open a security hole + if ($teaser) { + unset($node->body); + } + else { + unset($node->teaser); + } + + return theme('node', $node, $teaser, $page); +} + +/** + * Apply filters to a node in preparation for theming. + */ +function node_prepare($node, $teaser = FALSE) { + $node->readmore = (strlen($node->teaser) < strlen($node->body)); + if ($teaser == FALSE) { + $node->body = check_markup($node->body, $node->format, FALSE); + } + else { + $node->teaser = check_markup($node->teaser, $node->format, FALSE); + } + return $node; +} + +/** + * Generate a page displaying a single node, along with its comments. + */ +function node_show($node, $cid) { + $output = node_view($node, FALSE, TRUE); + + if (function_exists('comment_render') && $node->comment) { + $output .= comment_render($node, $cid); + } + + // Update the history table, stating that this user viewed this node. + node_tag_new($node->nid); + + return $output; +} + +/** + * Implementation of hook_perm(). + */ +function node_perm() { + $perms = array('administer content types', 'administer nodes', 'access content', 'view revisions', 'revert revisions'); + + foreach (node_get_types() as $type) { + if ($type->module == 'node') { + $name = check_plain($type->name); + $perms[] = 'create '. $name .' content'; + $perms[] = 'edit own '. $name .' content'; + $perms[] = 'edit '. $name .' content'; + } + } + + return $perms; +} + +/** + * Implementation of hook_search(). + */ +function node_search($op = 'search', $keys = NULL) { + switch ($op) { + case 'name': + return t('content'); + + case 'reset': + variable_del('node_cron_last'); + variable_del('node_cron_last_nid'); + return; + + case 'status': + $last = variable_get('node_cron_last', 0); + $last_nid = variable_get('node_cron_last_nid', 0); + $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')); + $remaining = db_result(db_query('SELECT COUNT(*) FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND ((GREATEST(n.created, n.changed, c.last_comment_timestamp) = %d AND n.nid > %d ) OR (n.created > %d OR n.changed > %d OR c.last_comment_timestamp > %d))', $last, $last_nid, $last, $last, $last)); + return array('remaining' => $remaining, 'total' => $total); + + case 'admin': + $form = array(); + // Output form for defining rank factor weights. + $form['content_ranking'] = array('#type' => 'fieldset', '#title' => t('Content ranking')); + $form['content_ranking']['#theme'] = 'node_search_admin'; + $form['content_ranking']['info'] = array('#type' => 'markup', '#value' => ''. t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') .''); + + $ranking = array('node_rank_relevance' => t('Keyword relevance'), + 'node_rank_recent' => t('Recently posted')); + if (module_exist('comment')) { + $ranking['node_rank_comments'] = t('Number of comments'); + } + if (module_exist('statistics') && variable_get('statistics_count_content_views', 0)) { + $ranking['node_rank_views'] = t('Number of views'); + } + + // Note: reversed to reflect that higher number = higher ranking. + $options = drupal_map_assoc(range(0, 10)); + foreach ($ranking as $var => $title) { + $form['content_ranking']['factors'][$var] = array('#title' => $title, '#type' => 'select', '#options' => $options, '#default_value' => variable_get($var, 5)); + } + return $form; + + case 'search': + // Build matching conditions + list($join1, $where1) = _db_rewrite_sql(); + $arguments1 = array(); + $conditions1 = 'n.status = 1'; + + if ($type = search_query_extract($keys, 'type')) { + $types = array(); + foreach (explode(',', $type) as $t) { + $types[] = "n.type = '%s'"; + $arguments1[] = $t; + } + $conditions1 .= ' AND ('. implode(' OR ', $types) .')'; + $keys = search_query_insert($keys, 'type'); + } + + if ($category = search_query_extract($keys, 'category')) { + $categories = array(); + foreach (explode(',', $category) as $c) { + $categories[] = "tn.tid = %d"; + $arguments1[] = $c; + } + $conditions1 .= ' AND ('. implode(' OR ', $categories) .')'; + $join1 .= ' INNER JOIN {term_node} tn ON n.nid = tn.nid'; + $keys = search_query_insert($keys, 'category'); + } + + // Build ranking expression (we try to map each parameter to a + // uniform distribution in the range 0..1). + $ranking = array(); + $arguments2 = array(); + $join2 = ''; + // Used to avoid joining on node_comment_statistics twice + $stats_join = FALSE; + if ($weight = (int)variable_get('node_rank_relevance', 5)) { + // Average relevance values hover around 0.15 + $ranking[] = '%d * i.relevance'; + $arguments2[] = $weight; + } + if ($weight = (int)variable_get('node_rank_recent', 5)) { + // Exponential decay with half-life of 6 months, starting at last indexed node + $ranking[] = '%d * POW(2, (GREATEST(n.created, n.changed, c.last_comment_timestamp) - %d) * 6.43e-8)'; + $arguments2[] = $weight; + $arguments2[] = (int)variable_get('node_cron_last', 0); + $join2 .= ' INNER JOIN {node} n ON n.nid = i.sid LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid'; + $stats_join = TRUE; + } + if (module_exist('comment') && $weight = (int)variable_get('node_rank_comments', 5)) { + // Inverse law that maps the highest reply count on the site to 1 and 0 to 0. + $scale = variable_get('node_cron_comments_scale', 0.0); + $ranking[] = '%d * (2.0 - 2.0 / (1.0 + c.comment_count * %f))'; + $arguments2[] = $weight; + $arguments2[] = $scale; + if (!$stats_join) { + $join2 .= ' LEFT JOIN {node_comment_statistics} c ON c.nid = i.sid'; + } + } + if (module_exist('statistics') && variable_get('statistics_count_content_views', 0) && + $weight = (int)variable_get('node_rank_views', 5)) { + // Inverse law that maps the highest view count on the site to 1 and 0 to 0. + $scale = variable_get('node_cron_views_scale', 0.0); + $ranking[] = '%d * (2.0 - 2.0 / (1.0 + nc.totalcount * %f))'; + $arguments2[] = $weight; + $arguments2[] = $scale; + $join2 .= ' LEFT JOIN {node_counter} nc ON nc.nid = i.sid'; + } + $select2 = (count($ranking) ? implode(' + ', $ranking) : 'i.relevance') . ' AS score'; + + // Do search + $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid '. $join1 .' INNER JOIN {users} u ON n.uid = u.uid', $conditions1 . (empty($where1) ? '' : ' AND '. $where1), $arguments1, $select2, $join2, $arguments2); + + // Load results + $results = array(); + foreach ($find as $item) { + $node = node_load($item->sid); + + // Get node output (filtered and with module-specific fields). + if (node_hook($node, 'view')) { + node_invoke($node, 'view', FALSE, FALSE); + } + else { + $node = node_prepare($node, FALSE); + } + // Allow modules to change $node->body before viewing. + node_invoke_nodeapi($node, 'view', FALSE, FALSE); + + // Fetch comments for snippet + $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index'); + // Fetch terms for snippet + $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index'); + + $extra = node_invoke_nodeapi($node, 'search result'); + $results[] = array('link' => url('node/'. $item->sid), + 'type' => node_get_types('name', $node), + 'title' => $node->title, + 'user' => theme('username', $node), + 'date' => $node->changed, + 'node' => $node, + 'extra' => $extra, + 'snippet' => search_excerpt($keys, $node->body)); + } + return $results; + } +} + +/** + * Implementation of hook_user(). + */ +function node_user($op, &$edit, &$user) { + if ($op == 'delete') { + db_query('UPDATE {node} SET uid = 0 WHERE uid = %d', $user->uid); + db_query('UPDATE {node_revisions} SET uid = 0 WHERE uid = %d', $user->uid); + } +} + +function theme_node_search_admin($form) { + $output = form_render($form['info']); + + $header = array(t('Factor'), t('Weight')); + foreach (element_children($form['factors']) as $key) { + $row = array(); + $row[] = $form['factors'][$key]['#title']; + unset($form['factors'][$key]['#title']); + $row[] = form_render($form['factors'][$key]); + $rows[] = $row; + } + $output .= theme('table', $header, $rows); + + $output .= form_render($form); + return $output; +} + +/** + * Menu callback; presents general node configuration options. + */ +function node_configure() { + + $form['default_nodes_main'] = array( + '#type' => 'select', '#title' => t('Number of posts on main page'), '#default_value' => variable_get('default_nodes_main', 10), + '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30)), + '#description' => t('The default maximum number of posts to display per page on overview pages such as the main page.') + ); + + $form['teaser_length'] = array( + '#type' => 'select', '#title' => t('Length of trimmed posts'), '#default_value' => variable_get('teaser_length', 600), + '#options' => array(0 => t('Unlimited'), 200 => t('200 characters'), 400 => t('400 characters'), 600 => t('600 characters'), + 800 => t('800 characters'), 1000 => t('1000 characters'), 1200 => t('1200 characters'), 1400 => t('1400 characters'), + 1600 => t('1600 characters'), 1800 => t('1800 characters'), 2000 => t('2000 characters')), + '#description' => t("The maximum number of characters used in the trimmed version of a post. Drupal will use this setting to determine at which offset long posts should be trimmed. The trimmed version of a post is typically used as a teaser when displaying the post on the main page, in XML feeds, etc. To disable teasers, set to 'Unlimited'. Note that this setting will only affect new or updated content and will not affect existing teasers.") + ); + + $form['node_preview'] = array( + '#type' => 'radios', '#title' => t('Preview post'), '#default_value' => variable_get('node_preview', 0), + '#options' => array(t('Optional'), t('Required')), '#description' => t('Must users preview posts before submitting?') + ); + + return system_settings_form('node_configure', $form); +} + +/** + * Retrieve the comment mode for the given node ID (none, read, or read/write). + */ +function node_comment_mode($nid) { + static $comment_mode; + if (!isset($comment_mode[$nid])) { + $comment_mode[$nid] = db_result(db_query('SELECT comment FROM {node} WHERE nid = %d', $nid)); + } + return $comment_mode[$nid]; +} + +/** + * Implementation of hook_link(). + */ +function node_link($type, $node = NULL, $teaser = FALSE) { + $links = array(); + + if ($type == 'node') { + if (array_key_exists('links', $node)) { + $links = $node->links; + } + + if ($teaser == 1 && $node->teaser && $node->readmore) { + $links['node_read_more'] = array( + 'title' => t('read more'), + 'href' => "node/$node->nid", + 'attributes' => array('title' => t('Read the rest of this posting.')) + ); + } + } + + return $links; +} + +/** + * Implementation of hook_menu(). + */ +function node_menu($may_cache) { + $items = array(); + if ($may_cache) { + $items[] = array('path' => 'admin/content', + 'title' => t('content management'), + 'description' => t('Manage your site\'s content.'), + 'position' => 'left', + 'weight' => -10, + 'callback' => 'system_admin_menu_block_page', + 'access' => user_access('access configuration pages'), + ); + + $items[] = array( + 'path' => 'admin/content/node', + 'title' => t('posts'), + 'description' => t('View, edit, and delete your site\'s content.'), + 'callback' => 'node_admin_nodes', + 'access' => user_access('administer nodes') + ); + + $items[] = array('path' => 'admin/content/node/overview', 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + + $items[] = array( + 'path' => 'admin/content/types', + 'title' => t('content types'), + 'description' => t('Manage posts by content type, including default status, front page promotion, etc.'), + 'callback' => 'node_overview_types', + 'access' => user_access('administer nodes'), + ); + $items[] = array( + 'path' => 'admin/content/types/list', + 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items[] = array( + 'path' => 'admin/content/types/add', + 'title' => t('add content type'), + 'callback' => 'node_type_form', + 'type' => MENU_LOCAL_TASK, + ); + + if (module_exist('search')) { + $items[] = array('path' => 'admin/content/search', 'title' => t('search posts'), + 'description' => t('Search posts by keyword.'), + 'callback' => 'node_admin_search', + 'access' => user_access('administer nodes'), + 'type' => MENU_NORMAL_ITEM); + } + + $items[] = array( + 'path' => 'admin/content/node-settings', + 'title' => t('post settings'), + 'description' => t('Control posting behavior, such as teaser length, requiring previews before posting, and the number of posts on the front page.'), + 'callback' => 'node_configure', + 'access' => user_access('administer nodes') + ); + + $items[] = array('path' => 'node', 'title' => t('content'), + 'callback' => 'node_page', + 'access' => user_access('access content'), + 'type' => MENU_MODIFIABLE_BY_ADMIN); + $items[] = array('path' => 'node/add', 'title' => t('create content'), + 'callback' => 'node_page', + 'access' => user_access('access content'), + 'type' => MENU_ITEM_GROUPING, + 'weight' => 1); + $items[] = array('path' => 'rss.xml', 'title' => t('rss feed'), + 'callback' => 'node_feed', + 'access' => user_access('access content'), + 'type' => MENU_CALLBACK); + + foreach (node_get_types() as $type) { + if (module_exist($type->module)) { + $name = check_plain($type->name); + $type_url_str = str_replace('_', '-', $type->type); + $items[] = array( + 'path' => 'node/add/'. $type_url_str, + 'title' => t($name), + 'access' => node_access('create', $type->type), + ); + } + } + } + else { + if (arg(0) == 'node' && is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node->nid) { + $items[] = array('path' => 'node/'. arg(1), 'title' => t('view'), + 'callback' => 'node_page', + 'access' => node_access('view', $node), + 'type' => MENU_CALLBACK); + $items[] = array('path' => 'node/'. arg(1) .'/view', 'title' => t('view'), + 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10); + $items[] = array('path' => 'node/'. arg(1) .'/edit', 'title' => t('edit'), + 'callback' => 'node_page', + 'access' => node_access('update', $node), + 'weight' => 1, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'node/'. arg(1) .'/delete', 'title' => t('delete'), + 'callback' => 'node_delete_confirm', + 'access' => node_access('delete', $node), + 'weight' => 1, + 'type' => MENU_CALLBACK); + $revisions_access = ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node) && db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', arg(1))) > 1); + $items[] = array('path' => 'node/'. arg(1) .'/revisions', 'title' => t('revisions'), + 'callback' => 'node_revisions', + 'access' => $revisions_access, + 'weight' => 2, + 'type' => MENU_LOCAL_TASK); + } + } + + // Content type configuration. + if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'types') { + include_once './'. drupal_get_path('module', 'node') .'/content_types.inc'; + + if (arg(3) != NULL) { + $type_name = arg(3); + $type_name = !empty($type_name) ? str_replace('-', '_', $type_name) : NULL; + $type = node_get_types('type', $type_name); + + if (!empty($type)) { + $type->name = check_plain($type->name); + $type_url_str = str_replace('_', '-', $type->type); + + $items[] = array( + 'path' => 'admin/content/types/'. $type_url_str, + 'title' => t($type->name), + 'callback' => 'node_type_form', + 'callback arguments' => array($type), + 'type' => MENU_CALLBACK, + ); + $items[] = array( + 'path' => 'admin/content/types/'. $type_url_str .'/delete', + 'title' => t('delete'), + 'callback' => 'node_type_delete', + 'callback arguments' => array($type), + 'type' => MENU_CALLBACK, + ); + } + } + } + } + + return $items; +} + +function node_last_changed($nid) { + $node = db_fetch_object(db_query('SELECT changed FROM {node} WHERE nid = %d', $nid)); + return ($node->changed); +} + +/** + * Implementation of hook_node_operations(). + */ +function node_node_operations() { + $operations = array( + 'approve' => array(t('Approve the selected posts'), 'node_operations_approve'), + 'promote' => array(t('Promote the selected posts'), 'node_operations_promote'), + 'sticky' => array(t('Make the selected posts sticky'), 'node_operations_sticky'), + 'demote' => array(t('Demote the selected posts'), 'node_operations_demote'), + 'unpublish' => array(t('Unpublish the selected posts'), 'node_operations_unpublish'), + 'delete' => array(t('Delete the selected posts'), ''), + ); + return $operations; +} + +/** + * Callback function for admin mass approving nodes. + */ +function node_operations_approve($nodes) { + db_query('UPDATE {node} SET status = 1 WHERE nid IN(%s)', implode(',', $nodes)); +} + +/** + * Callback function for admin mass promoting nodes. + */ +function node_operations_promote($nodes) { + db_query('UPDATE {node} SET status = 1, promote = 1 WHERE nid IN(%s)', implode(',', $nodes)); +} + +/** + * Callback function for admin mass editing nodes to be sticky. + */ +function node_operations_sticky($nodes) { + db_query('UPDATE {node} SET status = 1, sticky = 1 WHERE nid IN(%s)', implode(',', $nodes)); +} + +/** + * Callback function for admin mass demoting nodes. + */ +function node_operations_demote($nodes) { + db_query('UPDATE {node} SET promote = 0 WHERE nid IN(%s)', implode(',', $nodes)); +} + +/** + * Callback function for admin mass unpublishing nodes. + */ +function node_operations_unpublish($nodes) { + db_query('UPDATE {node} SET status = 0 WHERE nid IN(%s)', implode(',', $nodes)); +} + +/** + * List node administration filters that can be applied. + */ +function node_filters() { + // Regular filters + $filters['status'] = array('title' => t('status'), + 'options' => array('status-1' => t('published'), 'status-0' => t('not published'), + 'promote-1' => t('promoted'), 'promote-0' => t('not promoted'), + 'sticky-1' => t('sticky'), 'sticky-0' => t('not sticky'))); + $filters['type'] = array('title' => t('type'), 'options' => node_get_types('names')); + // The taxonomy filter + if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) { + $filters['category'] = array('title' => t('category'), 'options' => $taxonomy); + } + + return $filters; +} + +/** + * Build query for node administration filters based on session. + */ +function node_build_filter_query() { + $filters = node_filters(); + + // Build query + $where = $args = array(); + $join = ''; + foreach ($_SESSION['node_overview_filter'] as $index => $filter) { + list($key, $value) = $filter; + switch($key) { + case 'status': + // Note: no exploitable hole as $key/$value have already been checked when submitted + list($key, $value) = explode('-', $value, 2); + $where[] = 'n.'. $key .' = %d'; + break; + case 'category': + $table = "tn$index"; + $where[] = "$table.tid = %d"; + $join .= "INNER JOIN {term_node} $table ON n.nid = $table.nid "; + break; + case 'type': + $where[] = "n.type = '%s'"; + } + $args[] = $value; + } + $where = count($where) ? 'WHERE '. implode(' AND ', $where) : ''; + + return array('where' => $where, 'join' => $join, 'args' => $args); +} + +/** + * Return form for node administration filters. + */ +function node_filter_form() { + $session = &$_SESSION['node_overview_filter']; + $session = is_array($session) ? $session : array(); + $filters = node_filters(); + + $i = 0; + $form['filters'] = array('#type' => 'fieldset', + '#title' => t('Show only items where'), + '#theme' => 'node_filters', + ); + foreach ($session as $filter) { + list($type, $value) = $filter; + if ($type == 'category') { + // Load term name from DB rather than search and parse options array. + $value = module_invoke('taxonomy', 'get_term', $value); + $value = $value->name; + } + else { + $value = $filters[$type]['options'][$value]; + } + $string = ($i++ ? 'and where %a is %b' : '%a is %b'); + $form['filters']['current'][] = array('#value' => t($string, array('%a' => $filters[$type]['title'] , '%b' => $value))); + } + + foreach ($filters as $key => $filter) { + $names[$key] = $filter['title']; + $form['filters']['status'][$key] = array('#type' => 'select', '#options' => $filter['options']); + } + + $form['filters']['filter'] = array('#type' => 'radios', '#options' => $names, '#default_value' => 'status'); + $form['filters']['buttons']['submit'] = array('#type' => 'submit', '#value' => (count($session) ? t('Refine') : t('Filter'))); + if (count($session)) { + $form['filters']['buttons']['undo'] = array('#type' => 'submit', '#value' => t('Undo')); + $form['filters']['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset')); + } + + return drupal_get_form('node_filter_form', $form); +} + +/** + * Theme node administration filter form. + */ +function theme_node_filter_form(&$form) { + $output .= '
'; + $output .= form_render($form['filters']); + $output .= '
'; + $output .= form_render($form); + return $output; +} + +/** + * Theme node administraton filter selector. + */ +function theme_node_filters(&$form) { + $output .= '
'; + + return $output; +} + +/** + * Process result from node administration filter form. + */ +function node_filter_form_submit() { + global $form_values; + $op = $_POST['op']; + $filters = node_filters(); + switch ($op) { + case t('Filter'): + case t('Refine'): + if (isset($form_values['filter'])) { + $filter = $form_values['filter']; + + // Flatten the options array to accommodate hierarchical/nested options. + $flat_options = form_options_flatten($filters[$filter]['options']); + + if (isset($flat_options[$form_values[$filter]])) { + $_SESSION['node_overview_filter'][] = array($filter, $form_values[$filter]); + } + } + break; + case t('Undo'): + array_pop($_SESSION['node_overview_filter']); + break; + case t('Reset'): + $_SESSION['node_overview_filter'] = array(); + break; + } +} + +/** + * Submit the node administration update form. + */ +function node_admin_nodes_submit($form_id, $edit) { + $operations = module_invoke_all('node_operations'); + $operation = $operations[$edit['operation']]; + // Filter out unchecked nodes + $nodes = array_diff($edit['nodes'], array(0)); + if ($function = $operation[1]) { + call_user_func($function, $nodes); + cache_clear_all(); + drupal_set_message(t('The update has been performed.')); + } +} + +function node_admin_nodes_validate($form_id, $edit) { + $edit['nodes'] = array_diff($edit['nodes'], array(0)); + if (count($edit['nodes']) == 0) { + if ($edit['operation'] == 'delete') { + form_set_error('', t('Please select some items to perform the delete operation.')); + } + else { + form_set_error('', t('Please select some items to perform the update on.')); + } + } +} + +/** + * Menu callback: content administration. + */ +function node_admin_nodes() { + global $form_values; + $output = node_filter_form(); + + if ($_POST['edit']['operation'] == 'delete' && $_POST['edit']['nodes']) { + return node_multiple_delete_confirm(); + } + + $filter = node_build_filter_query(); + + $result = pager_query('SELECT n.*, u.name, u.uid FROM {node} n '. $filter['join'] .' INNER JOIN {users} u ON n.uid = u.uid '. $filter['where'] .' ORDER BY n.changed DESC', 50, 0, NULL, $filter['args']); + + $form['options'] = array('#type' => 'fieldset', + '#title' => t('Update options'), + '#prefix' => '
', + '#suffix' => '
', + ); + $options = array(); + foreach (module_invoke_all('node_operations') as $key => $value) { + $options[$key] = $value[0]; + } + $form['options']['operation'] = array('#type' => 'select', '#options' => $options, '#default_value' => 'approve'); + $form['options']['submit'] = array('#type' => 'submit', '#value' => t('Update')); + + $destination = drupal_get_destination(); + while ($node = db_fetch_object($result)) { + $nodes[$node->nid] = ''; + $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid) .' '. theme('mark', node_mark($node->nid, $node->changed))); + $form['name'][$node->nid] = array('#value' => node_get_types('name', $node)); + $form['username'][$node->nid] = array('#value' => theme('username', $node)); + $form['status'][$node->nid] = array('#value' => ($node->status ? t('published') : t('not published'))); + $form['operations'][$node->nid] = array('#value' => l(t('edit'), 'node/'. $node->nid .'/edit', array(), $destination)); + } + $form['nodes'] = array('#type' => 'checkboxes', '#options' => $nodes); + $form['pager'] = array('#value' => theme('pager', NULL, 50, 0)); + + // Call the form first, to allow for the form_values array to be populated. + $output .= drupal_get_form('node_admin_nodes', $form); + + return $output; +} + +/** + * Theme node administration overview. + */ +function theme_node_admin_nodes($form) { + // Overview table: + $header = array(NULL, t('Title'), t('Type'), t('Author'), t('Status'), t('Operations')); + + $output .= form_render($form['options']); + if (isset($form['title']) && is_array($form['title'])) { + foreach (element_children($form['title']) as $key) { + $row = array(); + $row[] = form_render($form['nodes'][$key]); + $row[] = form_render($form['title'][$key]); + $row[] = form_render($form['name'][$key]); + $row[] = form_render($form['username'][$key]); + $row[] = form_render($form['status'][$key]); + $row[] = form_render($form['operations'][$key]); + $rows[] = $row; + } + + } + else { + $rows[] = array(array('data' => t('No posts available.'), 'colspan' => '6')); + } + + $output .= theme('table', $header, $rows); + if ($form['pager']['#value']) { + $output .= form_render($form['pager']); + } + + $output .= form_render($form); + + return $output; +} + +function node_multiple_delete_confirm() { + $edit = $_POST['edit']; + + $form['nodes'] = array('#prefix' => '', '#tree' => TRUE); + // array_filter returns only elements with TRUE values + foreach (array_filter($edit['nodes']) as $nid => $value) { + $title = db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $nid)); + $form['nodes'][$nid] = array('#type' => 'hidden', '#value' => $nid, '#prefix' => '
  • ', '#suffix' => check_plain($title) ."
  • \n"); + } + $form['operation'] = array('#type' => 'hidden', '#value' => 'delete'); + + return confirm_form('node_multiple_delete_confirm', $form, + t('Are you sure you want to delete these items?'), + 'admin/content/node', t('This action cannot be undone.'), + t('Delete all'), t('Cancel')); +} + +function node_multiple_delete_confirm_submit($form_id, $edit) { + if ($edit['confirm']) { + foreach ($edit['nodes'] as $nid => $value) { + node_delete($nid); + } + drupal_set_message(t('The items have been deleted.')); + } + return 'admin/content/node'; +} + +/** + * Generate an overview table of older revisions of a node. + */ +function node_revision_overview($node) { + drupal_set_title(t('Revisions for %title', array('%title' => check_plain($node->title)))); + + $header = array(t('Revision'), array('data' => t('Operations'), 'colspan' => 2)); + + $revisions = node_revision_list($node); + + $rows = array(); + $revert_permission = FALSE; + if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) { + $revert_permission = TRUE; + } + $delete_permission = FALSE; + if (user_access('administer nodes')) { + $delete_permission = TRUE; + } + foreach ($revisions as $revision) { + $row = array(); + $operations = array(); + + if ($revision->current_vid > 0) { + $row[] = array('data' => t('%date by %username', array('%date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"), '%username' => theme('username', $revision))) + . (($revision->log != '') ? '

    '. filter_xss($revision->log) .'

    ' : ''), + 'class' => 'revision-current'); + $operations[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => 2); + } + else { + $row[] = t('%date by %username', array('%date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '%username' => theme('username', $revision))) + . (($revision->log != '') ? '

    '. filter_xss($revision->log) .'

    ' : ''); + if ($revert_permission) { + $operations[] = l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert"); + } + if ($delete_permission) { + $operations[] = l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete"); + } + } + $rows[] = array_merge($row, $operations); + } + $output .= theme('table', $header, $rows); + + return $output; +} + +/** + * Revert to the revision with the specified revision number. A node and nodeapi "update" event is triggered + * (via the node_save() call) when a revision is reverted. + */ +function node_revision_revert($nid, $revision) { + global $user; + + $node = node_load($nid, $revision); + if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) { + if ($node->vid) { + $node->revision = 1; + $node->log = t('Copy of the revision from %date.', array('%date' => theme('placeholder', format_date($node->revision_timestamp)))); + $node->taxonomy = array_keys($node->taxonomy); + + node_save($node); + + drupal_set_message(t('%title has been reverted back to the revision from %revision-date', array('%revision-date' => theme('placeholder', format_date($node->revision_timestamp)), '%title' => theme('placeholder', check_plain($node->title))))); + watchdog('content', t('%type: reverted %title revision %revision.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision)))); + } + else { + drupal_set_message(t('You tried to revert to an invalid revision.'), 'error'); + } + drupal_goto('node/'. $nid .'/revisions'); + } + drupal_access_denied(); +} + +/** + * Delete the revision with specified revision number. A "delete revision" nodeapi event is invoked when a + * revision is deleted. + */ +function node_revision_delete($nid, $revision) { + if (user_access('administer nodes')) { + $node = node_load($nid); + if (node_access('delete', $node)) { + // Don't delete the current revision + if ($revision != $node->vid) { + $node = node_load($nid, $revision); + + db_query("DELETE FROM {node_revisions} WHERE nid = %d AND vid = %d", $nid, $revision); + node_invoke_nodeapi($node, 'delete revision'); + drupal_set_message(t('Deleted %title revision %revision.', array('%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision)))); + watchdog('content', t('%type: deleted %title revision %revision.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%revision' => theme('placeholder', $revision)))); + } + + else { + drupal_set_message(t('Deletion failed. You tried to delete the current revision.')); + } + if (db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', $nid)) > 1) { + drupal_goto("node/$nid/revisions"); + } + else { + drupal_goto("node/$nid"); + } + } + } + + drupal_access_denied(); +} + +/** + * Return a list of all the existing revision numbers. + */ +function node_revision_list($node) { + $revisions = array(); + $result = db_query('SELECT r.vid, r.title, r.log, r.uid, n.vid AS current_vid, r.timestamp, u.name FROM {node_revisions} r LEFT JOIN {node} n ON n.vid = r.vid INNER JOIN {users} u ON u.uid = r.uid WHERE r.nid = %d ORDER BY r.timestamp DESC', $node->nid); + while ($revision = db_fetch_object($result)) { + $revisions[] = $revision; + } + + return $revisions; +} + +function node_admin_search() { + $output = search_form(url('admin/content/search'), $_POST['edit']['keys'], 'node') . search_data($_POST['edit']['keys'], 'node'); + return $output; +} + +/** + * Implementation of hook_block(). + */ +function node_block($op = 'list', $delta = 0) { + if ($op == 'list') { + $blocks[0]['info'] = t('Syndicate'); + return $blocks; + } + else if ($op == 'view') { + $block['subject'] = t('Syndicate'); + $block['content'] = theme('feed_icon', url('rss.xml')); + + return $block; + } +} + +/** + * A generic function for generating RSS feeds from a set of nodes. + * + * @param $nodes + * An object as returned by db_query() which contains the nid field. + * @param $channel + * An associative array containing title, link, description and other keys. + * The link should be an absolute URL. + */ +function node_feed($nodes = 0, $channel = array()) { + global $base_url, $locale; + + if (!$nodes) { + $nodes = db_query_range(db_rewrite_sql('SELECT n.nid, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.created DESC'), 0, variable_get('feed_default_items', 10)); + } + + $item_length = variable_get('feed_item_length', 'teaser'); + $namespaces = array('xmlns:dc="http://purl.org/dc/elements/1.1/"'); + + while ($node = db_fetch_object($nodes)) { + // Load the specified node: + $item = node_load($node->nid); + $link = url("node/$node->nid", NULL, NULL, 1); + + if ($item_length != 'title') { + $teaser = ($item_length == 'teaser') ? TRUE : FALSE; + + // Filter and prepare node teaser + if (node_hook($item, 'view')) { + node_invoke($item, 'view', $teaser, FALSE); + } + else { + $item = node_prepare($item, $teaser); + } + + // Allow modules to change $node->teaser before viewing. + node_invoke_nodeapi($item, 'view', $teaser, FALSE); + } + + // Allow modules to add additional item fields and/or modify $item + $extra = node_invoke_nodeapi($item, 'rss item'); + $extra = array_merge($extra, array(array('key' => 'pubDate', 'value' => date('r', $item->created)), array('key' => 'dc:creator', 'value' => $item->name), array('key' => 'guid', 'value' => $item->nid . ' at ' . $base_url, 'attributes' => array('isPermaLink' => 'FALSE')))); + foreach ($extra as $element) { + if ($element['namespace']) { + $namespaces = array_merge($namespaces, $element['namespace']); + } + } + + // Prepare the item description + switch ($item_length) { + case 'fulltext': + $item_text = $item->body; + break; + case 'teaser': + $item_text = $item->teaser; + if ($item->readmore) { + $item_text .= '

    '. l(t('read more'), 'node/'. $item->nid, NULL, NULL, NULL, TRUE) .'

    '; + } + break; + case 'title': + $item_text = ''; + break; + } + + $items .= format_rss_item($item->title, $link, $item_text, $extra); + } + + $channel_defaults = array( + 'version' => '2.0', + 'title' => variable_get('site_name', 'drupal') .' - '. variable_get('site_slogan', ''), + 'link' => $base_url, + 'description' => variable_get('site_mission', ''), + 'language' => $locale + ); + $channel = array_merge($channel_defaults, $channel); + + $output = "\n"; + $output .= "\n"; + $output .= format_rss_channel($channel['title'], $channel['link'], $channel['description'], $items, $channel['language']); + $output .= "\n"; + + drupal_set_header('Content-Type: application/rss+xml; charset=utf-8'); + print $output; +} + +/** + * Prepare node for save and allow modules to make changes. + */ +function node_submit($node) { + global $user; + + // Convert the node to an object, if necessary. + $node = (object)$node; + + // Auto-generate the teaser, but only if it hasn't been set (e.g. by a + // module-provided 'teaser' form item). + if (!isset($node->teaser)) { + $node->teaser = isset($node->body) ? node_teaser($node->body, isset($node->format) ? $node->format : NULL) : ''; + } + + $access = user_access('administer nodes'); + if ($access) { + // Populate the "authored by" field. + if ($account = user_load(array('name' => $node->name))) { + $node->uid = $account->uid; + } + else { + $node->uid = 0; + } + + $node->created = $node->date ? strtotime($node->date) : NULL; + } + // Force defaults in case people modify the form: + $node_options = variable_get('node_options_'. $node->type, array('status', 'promote')); + foreach (array('status', 'promote', 'sticky', 'revision') as $key) { + if (!$access || !isset($node->$key)) { + $node->$key = in_array($key, $node_options); + } + } + + // Do node-type-specific validation checks. + node_invoke($node, 'submit'); + node_invoke_nodeapi($node, 'submit'); + + $node->validated = TRUE; + + return $node; +} + +/** + * Perform validation checks on the given node. + */ +function node_validate($node, $form = array()) { + // Convert the node to an object, if necessary. + $node = (object)$node; + + // Make sure the body has the minimum number of words. + // todo use a better word counting algorithm that will work in other languages + if (isset($node->body) && count(explode(' ', $node->body)) < variable_get('minimum_'. $node->type .'_size', 0)) { + form_set_error('body', t('The body of your %type is too short. You need at least %words words.', array('%words' => variable_get('minimum_'. $node->type .'_size', 0), '%type' => node_get_types('name', $node)))); + } + + if (isset($node->nid) && (node_last_changed($node->nid) > $_POST['edit']['changed'])) { + form_set_error('changed', t('This content has been modified by another user, changes cannot be saved.')); + } + + if (user_access('administer nodes')) { + // Validate the "authored by" field. + if (!empty($node->name) && !($account = user_load(array('name' => $node->name)))) { + // The use of empty() is mandatory in the context of usernames + // as the empty string denotes the anonymous user. In case we + // are dealing with an anonymous user we set the user ID to 0. + form_set_error('name', t('The username %name does not exist.', array ('%name' => theme('placeholder', $node->name)))); + } + + // Validate the "authored on" field. As of PHP 5.1.0, strtotime returns FALSE instead of -1 upon failure. + if (!empty($node->date) && strtotime($node->date) <= 0) { + form_set_error('date', t('You have to specify a valid date.')); + } + } + + // Do node-type-specific validation checks. + node_invoke($node, 'validate', $form); + node_invoke_nodeapi($node, 'validate', $form); +} + +function node_form_validate($form_id, $form_values, $form) { + node_validate($form_values, $form); +} + +function node_object_prepare(&$node) { + if (user_access('administer nodes')) { + // Set up default values, if required. + if (!isset($node->created)) { + $node->created = time(); + } + + if (!isset($node->date)) { + $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O'); + } + } + node_invoke($node, 'prepare'); + node_invoke_nodeapi($node, 'prepare'); +} + +/** + * Generate the node editing form. + */ +function node_form($node) { + $node = (object)$node; + $form = node_form_array($node); + return drupal_get_form($node->type .'_node_form', $form, 'node_form'); +} + +/** + * Generate the node editing form array. + */ +function node_form_array($node) { + node_object_prepare($node); + + // Set the id of the top-level form tag + $form['#id'] = 'node-form'; + + /** + * Basic node information. + * These elements are just values so they are not even sent to the client. + */ + foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) { + $form[$key] = array('#type' => 'value', '#value' => $node->$key); + } + + // Changed must be sent to the client, for later overwrite error checking. + $form['changed'] = array('#type' => 'hidden', '#value' => $node->changed); + + // Get the node-specific bits. + $form = array_merge_recursive($form, node_invoke($node, 'form')); + if (!isset($form['title']['#weight'])) { + $form['title']['#weight'] = -5; + } + + $node_options = variable_get('node_options_'. $node->type, array('status', 'promote')); + // If this is a new node, fill in the default values. + if (!isset($node->nid)) { + foreach (array('status', 'promote', 'sticky', 'revision') as $key) { + $node->$key = in_array($key, $node_options); + } + global $user; + $node->uid = $user->uid; + } + else { + // Nodes being edited should always be preset with the default revision setting. + $node->revision = in_array('revision', $node_options); + } + $form['#node'] = $node; + + if (user_access('administer nodes')) { + // Node author information + $form['author'] = array('#type' => 'fieldset', '#title' => t('Authoring information'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 20); + $form['author']['name'] = array('#type' => 'textfield', '#title' => t('Authored by'), '#maxlength' => 60, '#autocomplete_path' => 'user/autocomplete', '#default_value' => $node->name ? $node->name : '', '#weight' => -1, '#description' => t('Leave blank for %anonymous.', array('%anonymous' => theme('placeholder', variable_get('anonymous', 'Anonymous'))))); + $form['author']['date'] = array('#type' => 'textfield', '#title' => t('Authored on'), '#maxlength' => 25, '#description' => t('Format: %time. Leave blank to use the time of form submission.', array('%time' => $node->date))); + + if (isset($node->nid)) { + $form['author']['date']['#default_value'] = $node->date; + } + + // Node options for administrators + $form['options'] = array('#type' => 'fieldset', '#title' => t('Publishing options'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#weight' => 25); + $form['options']['status'] = array('#type' => 'checkbox', '#title' => t('Published'), '#default_value' => $node->status); + $form['options']['promote'] = array('#type' => 'checkbox', '#title' => t('Promoted to front page'), '#default_value' => $node->promote); + $form['options']['sticky'] = array('#type' => 'checkbox', '#title' => t('Sticky at top of lists'), '#default_value' => $node->sticky); + $form['options']['revision'] = array('#type' => 'checkbox', '#title' => t('Create new revision'), '#default_value' => $node->revision); + } + else { + // Put all of these through as values if the user doesn't have access to them. + foreach (array('uid', 'created') as $key) { + $form[$key] = array('#type' => 'value', '#value' => $node->$key); + } + } + + // Add the buttons. + $form['preview'] = array('#type' => 'button', '#value' => t('Preview'), '#weight' => 40); + $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'), '#weight' => 45); + if ($node->nid && node_access('delete', $node)) { + $form['delete'] = array('#type' => 'button', '#value' => t('Delete'), '#weight' => 50); + } + + $form['#after_build'] = array('node_form_add_preview'); + + return $form; +} + +function node_form_add_preview($form) { + global $form_values; + + $op = isset($_POST['op']) ? $_POST['op'] : ''; + if ($op == t('Preview')) { + drupal_validate_form($form['form_id']['#value'], $form); + if (!form_get_errors()) { + // We pass the global $form_values here to preserve changes made during form validation + $form['node_preview'] = array('#value' => node_preview((object)$form_values), '#weight' => -100); + } + } + if (variable_get('node_preview', 0) && (form_get_errors() || $op != t('Preview'))) { + unset($form['submit']); + } + return $form; +} + +function theme_node_form($form) { + $output = "\n
    \n"; + if (isset($form['node_preview'])) { + $output .= form_render($form['node_preview']); + } + + // Admin form fields and submit buttons must be rendered first, because + // they need to go to the bottom of the form, and so should not be part of + // the catch-all call to form_render(). + $admin = ''; + if (isset($form['author'])) { + $admin .= "
    \n"; + $admin .= form_render($form['author']); + $admin .= "
    \n"; + } + if (isset($form['options'])) { + $admin .= "
    \n"; + $admin .= form_render($form['options']); + $admin .= "
    \n"; + } + $buttons = form_render($form['preview']); + $buttons .= form_render($form['submit']); + $buttons .= isset($form['delete']) ? form_render($form['delete']) : ''; + + // Everything else gets rendered here, and is displayed before the admin form + // field and the submit buttons. + $output .= "
    \n"; + $output .= form_render($form); + $output .= "
    \n"; + + if (!empty($admin)) { + $output .= "
    \n"; + $output .= $admin; + $output .= "
    \n"; + } + $output .= $buttons; + $output .= "
    \n"; + + return $output; +} + +/** + * Present a node submission form or a set of links to such forms. + */ +function node_add($type) { + global $user; + + $types = node_get_types(); + $type = isset($type) ? str_replace('-', '_', $type) : NULL; + // If a node type has been specified, validate its existence. + if (isset($types[$type]) && node_access('create', $type)) { + // Initialize settings: + $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => $type); + + $output = node_form($node); + drupal_set_title(t('Submit %name', array('%name' => check_plain($types[$type]->name)))); + } + else { + // If no (valid) node type has been provided, display a node type overview. + foreach ($types as $type) { + if (module_exist($type->module) && node_access('create', $type->type)) { + $type_url_str = str_replace('_', '-', $type->type); + $title = t('Add a new %s.', array('%s' => check_plain($type->name))); + $out = '
    '. l($type->name, "node/add/$type_url_str", array('title' => $title)) .'
    '; + $out .= '
    '. filter_xss_admin($type->description) .'
    '; + $item[$type->type] = $out; + } + } + + if (isset($item)) { + uksort($item, 'strnatcasecmp'); + $output = t('Choose the appropriate item from the list:') .'
    '. implode('', $item) .'
    '; + } + else { + $output = t('No content types available.'); + } + } + + return $output; +} + +/** + * Generate a node preview. + */ +function node_preview($node) { + if (node_access('create', $node) || node_access('update', $node)) { + // Load the user's name when needed: + if (isset($node->name)) { + // The use of isset() is mandatory in the context of user IDs, because + // user ID 0 denotes the anonymous user. + if ($user = user_load(array('name' => $node->name))) { + $node->uid = $user->uid; + } + else { + $node->uid = 0; // anonymous user + } + } + else if ($node->uid) { + $user = user_load(array('uid' => $node->uid)); + $node->name = $user->name; + } + + // Set the timestamps when needed: + if ($node->date) { + $node->created = strtotime($node->date); + } + $node->changed = time(); + + // Extract a teaser, if it hasn't been set (e.g. by a module-provided + // 'teaser' form item). + if (!isset($node->teaser)) { + $node->teaser = node_teaser($node->body, $node->format); + } + + // Display a preview of the node: + // Previewing alters $node so it needs to be cloned. + if (!form_get_errors()) { + $cloned_node = drupal_clone($node); + $cloned_node->in_preview = TRUE; + $output = theme('node_preview', $cloned_node); + } + drupal_set_title(t('Preview')); + drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('create content'), 'node/add'), l(t('Submit %name', array('%name' => node_get_types('name', $node))), 'node/add/'. $node->type))); + + return $output; + } +} + +/** + * Display a node preview for display during node creation and editing. + * + * @param $node + * The node object which is being previewed. + */ +function theme_node_preview($node) { + $output = '
    '; + if ($node->teaser && $node->teaser != $node->body) { + drupal_set_message(t('The trimmed version of your post shows what your post looks like when promoted to the main page or when exported for syndication. You can insert the delimiter "<!--break-->" (without the quotes) to fine-tune where your post gets split.')); + $output .= '

    '. t('Preview trimmed version') .'

    '; + $output .= node_view(drupal_clone($node), 1, FALSE, 0); + $output .= '

    '. t('Preview full version') .'

    '; + $output .= node_view($node, 0, FALSE, 0); + } + else { + $output .= node_view($node, 0, FALSE, 0); + } + $output .= "
    \n"; + + return $output; +} + +function node_form_submit($form_id, $edit) { + global $user; + + // Fix up the node when required: + $node = node_submit($edit); + + // Prepare the node's body: + if ($node->nid) { + // Check whether the current user has the proper access rights to + // perform this operation: + if (node_access('update', $node)) { + node_save($node); + watchdog('content', t('%type: updated %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid)); + drupal_set_message(t('The %post was updated.', array ('%post' => node_get_types('name', $node)))); + } + } + else { + // Check whether the current user has the proper access rights to + // perform this operation: + if (node_access('create', $node)) { + node_save($node); + watchdog('content', t('%type: added %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid")); + drupal_set_message(t('Your %post was created.', array ('%post' => node_get_types('name', $node)))); + } + } + if ($node->nid) { + if (node_access('view', $node)) { + return 'node/'. $node->nid; + } + else { + return ''; + } + } + // it is very unlikely we get here + return FALSE; +} + +/** + * Menu callback -- ask for confirmation of node deletion + */ +function node_delete_confirm() { + $edit = $_POST['edit']; + $edit['nid'] = $edit['nid'] ? $edit['nid'] : arg(1); + $node = node_load($edit['nid']); + + if (node_access('delete', $node)) { + $form['nid'] = array('#type' => 'value', '#value' => $node->nid); + $output = confirm_form('node_delete_confirm', $form, + t('Are you sure you want to delete %title?', array('%title' => theme('placeholder', $node->title))), + $_GET['destination'] ? $_GET['destination'] : 'node/'. $node->nid, t('This action cannot be undone.'), + t('Delete'), t('Cancel') ); + } + + return $output; +} + +/** + * Execute node deletion + */ +function node_delete_confirm_submit($form_id, $form_values) { + if ($form_values['confirm']) { + node_delete($form_values['nid']); + } + + return ''; +} + +/** + * Delete a node. + */ +function node_delete($nid) { + + $node = node_load($nid); + + if (node_access('delete', $node)) { + db_query('DELETE FROM {node} WHERE nid = %d', $node->nid); + db_query('DELETE FROM {node_revisions} WHERE nid = %d', $node->nid); + + // Call the node-specific callback (if any): + node_invoke($node, 'delete'); + node_invoke_nodeapi($node, 'delete'); + + // Clear the cache so an anonymous poster can see the node being deleted. + cache_clear_all(); + + // Remove this node from the search index if needed. + if (function_exists('search_wipe')) { + search_wipe($node->nid, 'node'); + } + drupal_set_message(t('%title has been deleted.', array('%title' => theme('placeholder', $node->title)))); + watchdog('content', t('%type: deleted %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title)))); + } +} + +/** + * Menu callback for revisions related activities. + */ +function node_revisions() { + if (is_numeric(arg(1)) && arg(2) == 'revisions') { + $op = arg(4) ? arg(4) : 'overview'; + switch ($op) { + case 'overview': + $node = node_load(arg(1)); + if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) { + return node_revision_overview($node); + } + drupal_access_denied(); + return; + case 'view': + if (is_numeric(arg(3))) { + $node = node_load(arg(1), arg(3)); + if ($node->nid) { + if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) { + drupal_set_title(t('Revision of %title from %date', array('%title' => theme('placeholder', $node->title), '%date' => format_date($node->revision_timestamp)))); + return node_show($node, arg(2)); + } + drupal_access_denied(); + return; + } + } + break; + case 'revert': + node_revision_revert(arg(1), arg(3)); + break; + case 'delete': + node_revision_delete(arg(1), arg(3)); + break; + } + } + drupal_not_found(); +} + +/** + * Generate a listing of promoted nodes. + */ +function node_page_default() { + $result = pager_query(db_rewrite_sql('SELECT n.nid, n.sticky, n.created FROM {node} n WHERE n.promote = 1 AND n.status = 1 ORDER BY n.sticky DESC, n.created DESC'), variable_get('default_nodes_main', 10)); + + if (db_num_rows($result)) { + drupal_add_link(array('rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => t('RSS'), + 'href' => url('rss.xml', NULL, NULL, TRUE))); + + $output = ''; + while ($node = db_fetch_object($result)) { + $output .= node_view(node_load($node->nid), 1); + } + $output .= theme('pager', NULL, variable_get('default_nodes_main', 10)); + } + else { + $output = t(' +

    Welcome to your new Drupal website!

    +

    Please follow these steps to set up and start using your website:

    +
      +
    1. + Create your administrator account + To begin, create the first account. This account will have full administration rights and will allow you to configure your website. +
    2. +
    3. + Configure your website + Once logged in, visit the administration section, where you can customize and configure all aspects of your website. +
    4. +
    5. + Enable additional functionality + Next, visit the module list and enable features which suit your specific needs. You can find additional modules in the Drupal modules download section. +
    6. +
    7. + Customize your website design + To change the "look and feel" of your website, visit the themes section. You may choose from one of the included themes or download additional themes from the Drupal themes download section. +
    8. +
    9. + Start posting content + Finally, you can create content for your website. This message will disappear once you have published your first post. +
    10. +
    +

    For more information, please refer to the help section, or the online Drupal handbooks. You may also post at the Drupal forum, or view the wide range of other support options available.

    ', + array('%drupal' => 'http://drupal.org/', '%register' => url('user/register'), '%admin' => url('admin'), '%config' => url('admin/settings'), '%modules' => url('admin/settings/modules'), '%download_modules' => 'http://drupal.org/project/modules', '%themes' => url('admin/themes'), '%download_themes' => 'http://drupal.org/project/themes', '%content' => url('node/add'), '%help' => url('admin/help'), '%handbook' => 'http://drupal.org/handbooks', '%forum' => 'http://drupal.org/forum', '%support' => 'http://drupal.org/support') + ); + $output = '
    '. $output .'
    '; + } + + return $output; +} + +/** + * Menu callback; dispatches control to the appropriate operation handler. + */ +function node_page() { + $op = arg(1); + + if (is_numeric($op)) { + $op = (arg(2) && !is_numeric(arg(2))) ? arg(2) : 'view'; + } + + switch ($op) { + case 'view': + if (is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node->nid) { + drupal_set_title(check_plain($node->title)); + return node_show($node, arg(2)); + } + else if (db_result(db_query('SELECT nid FROM {node} WHERE nid = %d', arg(1)))) { + drupal_access_denied(); + } + else { + drupal_not_found(); + } + } + break; + case 'add': + return node_add(arg(2)); + break; + case 'edit': + if ($_POST['op'] == t('Delete')) { + // Note: we redirect from node/uid/edit to node/uid/delete to make the tabs disappear. + if ($_REQUEST['destination']) { + $destination = drupal_get_destination(); + unset($_REQUEST['destination']); + } + drupal_goto('node/'. arg(1) .'/delete', $destination); + } + + if (is_numeric(arg(1))) { + $node = node_load(arg(1)); + if ($node->nid) { + drupal_set_title(check_plain($node->title)); + return node_form($node); + } + else if (db_result(db_query('SELECT nid FROM {node} WHERE nid = %d', arg(1)))) { + drupal_access_denied(); + } + else { + drupal_not_found(); + } + } + break; + default: + drupal_set_title(''); + return node_page_default(); + } +} + +/** + * shutdown function to make sure we always mark the last node processed. + */ +function node_update_shutdown() { + global $last_change, $last_nid; + + if ($last_change && $last_nid) { + variable_set('node_cron_last', $last_change); + variable_set('node_cron_last_nid', $last_nid); + } +} + +/** + * Implementation of hook_update_index(). + */ +function node_update_index() { + global $last_change, $last_nid; + + register_shutdown_function('node_update_shutdown'); + + $last = variable_get('node_cron_last', 0); + $last_nid = variable_get('node_cron_last_nid', 0); + $limit = (int)variable_get('search_cron_limit', 100); + + // Store the maximum possible comments per thread (used for ranking by reply count) + variable_set('node_cron_comments_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')))); + variable_set('node_cron_views_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(totalcount) FROM {node_counter}')))); + + $result = db_query_range('SELECT GREATEST(c.last_comment_timestamp, n.changed) as last_change, n.nid FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND ((GREATEST(n.changed, c.last_comment_timestamp) = %d AND n.nid > %d) OR (n.changed > %d OR c.last_comment_timestamp > %d)) ORDER BY GREATEST(n.changed, c.last_comment_timestamp) ASC, n.nid ASC', $last, $last_nid, $last, $last, $last, 0, $limit); + + while ($node = db_fetch_object($result)) { + $last_change = $node->last_change; + $last_nid = $node->nid; + $node = node_load($node->nid); + + // Get node output (filtered and with module-specific fields). + if (node_hook($node, 'view')) { + node_invoke($node, 'view', FALSE, FALSE); + } + else { + $node = node_prepare($node, FALSE); + } + // Allow modules to change $node->body before viewing. + node_invoke_nodeapi($node, 'view', FALSE, FALSE); + + $text = '

    '. check_plain($node->title) .'

    '. $node->body; + + // Fetch extra data normally not visible + $extra = node_invoke_nodeapi($node, 'update index'); + foreach ($extra as $t) { + $text .= $t; + } + + // Update index + search_index($node->nid, 'node', $text); + } +} + +/** + * Implementation of hook_form_alter(). + */ +function node_form_alter($form_id, &$form) { + // Advanced node search form + if ($form_id == 'search_form' && arg(1) == 'node' && user_access('use advanced search')) { + // Keyword boxes: + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced search'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => array('class' => 'search-advanced'), + ); + $form['advanced']['keywords'] = array( + '#prefix' => '
    ', + '#suffix' => '
    ', + ); + $form['advanced']['keywords']['or'] = array( + '#type' => 'textfield', + '#title' => t('Containing any of the words'), + '#size' => 30, + '#maxlength' => 255, + ); + $form['advanced']['keywords']['phrase'] = array( + '#type' => 'textfield', + '#title' => t('Containing the phrase'), + '#size' => 30, + '#maxlength' => 255, + ); + $form['advanced']['keywords']['negative'] = array( + '#type' => 'textfield', + '#title' => t('Containing none of the words'), + '#size' => 30, + '#maxlength' => 255, + ); + + // Taxonomy box: + if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) { + $form['advanced']['category'] = array( + '#type' => 'select', + '#title' => t('Only in the category(s)'), + '#prefix' => '
    ', + '#size' => 10, + '#suffix' => '
    ', + '#options' => $taxonomy, + '#multiple' => TRUE, + ); + } + + // Node types: + $types = node_get_types(); + $form['advanced']['type'] = array( + '#type' => 'checkboxes', + '#title' => t('Only of the type(s)'), + '#prefix' => '
    ', + '#suffix' => '
    ', + '#options' => $types, + ); + $form['advanced']['submit'] = array( + '#type' => 'submit', + '#value' => t('Advanced search'), + '#prefix' => '
    ', + '#suffix' => '

    ', + ); + + $form['#validate']['node_search_validate'] = array(); + } +} + +/** + * Form API callback for the search form. Registered in node_form_alter(). + */ +function node_search_validate($form_id, $form_values, $form) { + // Initialise using any existing basic search keywords. + $keys = $form_values['processed_keys']; + + // Insert extra restrictions into the search keywords string. + if (isset($form_values['type']) && is_array($form_values['type'])) { + // Retrieve selected types - Forms API sets the value of unselected checkboxes to 0. + $form_values['type'] = array_filter($form_values['type']); + if (count($form_values['type'])) { + $keys = search_query_insert($keys, 'type', implode(',', array_keys($form_values['type']))); + } + } + + if (isset($form_values['category']) && is_array($form_values['category'])) { + $keys = search_query_insert($keys, 'category', implode(',', $form_values['category'])); + } + if ($form_values['or'] != '') { + if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_values['or'], $matches)) { + $keys .= ' '. implode(' OR ', $matches[1]); + } + } + if ($form_values['negative'] != '') { + if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' '. $form_values['negative'], $matches)) { + $keys .= ' -'. implode(' -', $matches[1]); + } + } + if ($form_values['phrase'] != '') { + $keys .= ' "'. str_replace('"', ' ', $form_values['phrase']) .'"'; + } + if (!empty($keys)) { + form_set_value($form['basic']['inline']['processed_keys'], trim($keys)); + } +} + +/** + * @defgroup node_access Node access rights + * @{ + * The node access system determines who can do what to which nodes. + * + * In determining access rights for a node, node_access() first checks + * whether the user has the "administer nodes" permission. Such users have + * unrestricted access to all nodes. Then the node module's hook_access() + * is called, and a TRUE or FALSE return value will grant or deny access. + * This allows, for example, the blog module to always grant access to the + * blog author, and for the book module to always deny editing access to + * PHP pages. + * + * If node module does not intervene (returns NULL), then the + * node_access table is used to determine access. All node access + * modules are queried using hook_node_grants() to assemble a list of + * "grant IDs" for the user. This list is compared against the table. + * If any row contains the node ID in question (or 0, which stands for "all + * nodes"), one of the grant IDs returned, and a value of TRUE for the + * operation in question, then access is granted. Note that this table is a + * list of grants; any matching row is sufficient to grant access to the + * node. + * + * In node listings, the process above is followed except that + * hook_access() is not called on each node for performance reasons and for + * proper functioning of the pager system. When adding a node listing to your + * module, be sure to use db_rewrite_sql() to add + * the appropriate clauses to your query for access checks. + * + * To see how to write a node access module of your own, see + * node_access_example.module. + */ + +/** + * Determine whether the current user may perform the given operation on the + * specified node. + * + * @param $op + * The operation to be performed on the node. Possible values are: + * - "view" + * - "update" + * - "delete" + * - "create" + * @param $node + * The node object (or node array) on which the operation is to be performed, + * or node type (e.g. 'forum') for "create" operation. + * @param $uid + * The user ID on which the operation is to be performed. + * @return + * TRUE if the operation may be performed. + */ +function node_access($op, $node = NULL, $uid = NULL) { + // Convert the node to an object if necessary: + if ($op != 'create') { + $node = (object)$node; + } + // If the node is in a restricted format, disallow editing. + if ($op == 'update' && !filter_access($node->format)) { + return FALSE; + } + + if (user_access('administer nodes')) { + return TRUE; + } + + if (!user_access('access content')) { + return FALSE; + } + + // Can't use node_invoke(), because the access hook takes the $op parameter + // before the $node parameter. + $module = node_get_types('module', $node); + if ($module == 'node') { + $module = 'node_content'; // Avoid function name collisions. + } + $access = module_invoke($module, 'access', $op, $node); + if (!is_null($access)) { + return $access; + } + + // If the module did not override the access rights, use those set in the + // node_access table. + if ($op != 'create' && $node->nid && $node->status) { + $grants = array(); + foreach (node_access_grants($op, $uid) as $realm => $gids) { + foreach ($gids as $gid) { + $grants[] = "(gid = $gid AND realm = '$realm')"; + } + } + + $grants_sql = ''; + if (count($grants)) { + $grants_sql = 'AND ('. implode(' OR ', $grants) .')'; + } + + $sql = "SELECT COUNT(*) FROM {node_access} WHERE (nid = 0 OR nid = %d) $grants_sql AND grant_$op >= 1"; + $result = db_query($sql, $node->nid); + return (db_result($result)); + } + return FALSE; +} + +/** + * Generate an SQL join clause for use in fetching a node listing. + * + * @param $node_alias + * If the node table has been given an SQL alias other than the default + * "n", that must be passed here. + * @param $node_access_alias + * If the node_access table has been given an SQL alias other than the default + * "na", that must be passed here. + * @return + * An SQL join clause. + */ +function _node_access_join_sql($node_alias = 'n', $node_access_alias = 'na') { + if (user_access('administer nodes')) { + return ''; + } + + return 'INNER JOIN {node_access} '. $node_access_alias .' ON '. $node_access_alias .'.nid = '. $node_alias .'.nid'; +} + +/** + * Generate an SQL where clause for use in fetching a node listing. + * + * @param $op + * The operation that must be allowed to return a node. + * @param $node_access_alias + * If the node_access table has been given an SQL alias other than the default + * "na", that must be passed here. + * @return + * An SQL where clause. + */ +function _node_access_where_sql($op = 'view', $node_access_alias = 'na', $uid = NULL) { + if (user_access('administer nodes')) { + return; + } + + $grants = array(); + foreach (node_access_grants($op, $uid) as $realm => $gids) { + foreach ($gids as $gid) { + $grants[] = "($node_access_alias.gid = $gid AND $node_access_alias.realm = '$realm')"; + } + } + + $grants_sql = ''; + if (count($grants)) { + $grants_sql = 'AND ('. implode(' OR ', $grants) .')'; + } + + $sql = "$node_access_alias.grant_$op >= 1 $grants_sql"; + return $sql; +} + +/** + * Fetch an array of permission IDs granted to the given user ID. + * + * The implementation here provides only the universal "all" grant. A node + * access module should implement hook_node_grants() to provide a grant + * list for the user. + * + * @param $op + * The operation that the user is trying to perform. + * @param $uid + * The user ID performing the operation. If omitted, the current user is used. + * @return + * An associative array in which the keys are realms, and the values are + * arrays of grants for those realms. + */ +function node_access_grants($op, $uid = NULL) { + global $user; + + if (isset($uid)) { + $user_object = user_load(array('uid' => $uid)); + } + else { + $user_object = $user; + } + + return array_merge(array('all' => array(0)), module_invoke_all('node_grants', $user_object, $op)); +} + +/** + * Determine whether the user has a global viewing grant for all nodes. + */ +function node_access_view_all_nodes() { + static $access; + + if (!isset($access)) { + $grants = array(); + foreach (node_access_grants('view') as $realm => $gids) { + foreach ($gids as $gid) { + $grants[] = "(gid = $gid AND realm = '$realm')"; + } + } + + $grants_sql = ''; + if (count($grants)) { + $grants_sql = 'AND ('. implode(' OR ', $grants) .')'; + } + + $sql = "SELECT COUNT(*) FROM {node_access} WHERE nid = 0 $grants_sql AND grant_view >= 1"; + $result = db_query($sql); + $access = db_result($result); + } + + return $access; +} + +/** + * Implementation of hook_db_rewrite_sql + */ +function node_db_rewrite_sql($query, $primary_table, $primary_field) { + if ($primary_field == 'nid' && !node_access_view_all_nodes()) { + $return['join'] = _node_access_join_sql($primary_table); + $return['where'] = _node_access_where_sql(); + $return['distinct'] = 1; + return $return; + } +} + +/** + * @} End of "defgroup node_access". + */ + + +/** + * @defgroup node_content Hook implementations for user-created content types. + * @{ + */ + +/** + * Implementation of hook_access(). + */ +function node_content_access($op, $node) { + global $user; + $type = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type); + + if ($op == 'create') { + return user_access('create '. $type .' content'); + } + + if ($op == 'update' || $op == 'delete') { + if (user_access('edit '. $type .' content') || (user_access('edit own '. $type .' content') && ($user->uid == $node->uid))) { + return TRUE; + } + } +} + +/** + * Implementation of hook_form(). + */ +function node_content_form($node) { + $type = node_get_types('type', $node); + + if ($type->has_title) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => check_plain($type->title_label), + '#required' => TRUE, + '#default_value' => $node->title, + '#weight' => -5, + ); + } + + if ($type->has_body) { + $form['body_filter']['body'] = array( + '#type' => 'textarea', + '#title' => check_plain($type->body_label), + '#default_value' => $node->body, + '#rows' => 20, + '#required' => TRUE); + $form['body_filter']['format'] = filter_form($node->format); + } + + return $form; +} + +/** + * @} End of "defgroup node_content". + */ diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/node/content_types.inc ./modules/node/content_types.inc --- ../C-HEAD/modules/node/content_types.inc 1969-12-31 16:00:00.000000000 -0800 +++ ./modules/node/content_types.inc 2006-08-01 00:23:23.000000000 -0700 @@ -0,0 +1,388 @@ + t('Operations'), 'colspan' => '2')); + $rows = array(); + + foreach ($names as $key => $name) { + $type = $types[$key]; + if (module_exist($type->module)) { + $name = check_plain($name); + $type_url_str = str_replace('_', '-', $type->type); + // Populate the operations field. + $operations = array(); + + // Set the edit column. + $operations[] = array('data' => l(t('edit'), 'admin/content/types/'. $type_url_str)); + + // Set the delete column. + if ($type->custom) { + $operations[] = array('data' => l(t('delete'), 'admin/content/types/'. $type_url_str .'/delete')); + } + else { + $operations[] = array('data' => ''); + } + + $row = array(array('data' => l($name, 'admin/content/types/'. $type_url_str), 'class' => $class), array('data' => check_plain($type->type), 'class' => $class), array('data' => check_plain($type->description), 'class' => $class)); + foreach ($operations as $operation) { + $operation['class'] = $class; + $row[] = $operation; + } + $rows[] = $row; + } + } + + if (empty($rows)) { + $rows[] = array(array('data' => t('No content types available.'), 'colspan' => '5', 'class' => 'message')); + } + + return theme('table', $header, $rows); +} + +/** + * Generates the node type editing form. + */ +function node_type_form($type = NULL) { + if (!isset($type->type)) { + $type = new stdClass(); + $type->type = $type->name = $type->module = $type->description = $type->help = ''; + $type->min_word_count = 0; + $type->has_title = TRUE; + $type->has_body = TRUE; + $type->title_label = t('Title'); + $type->body_label = t('Body'); + $type->custom = TRUE; + $type->modified = FALSE; + $type->locked = FALSE; + } + + $form['identity'] = array( + '#type' => 'fieldset', + '#title' => t('Identification'), + ); + $form['identity']['name'] = array( + '#title' => t('Name'), + '#type' => 'textfield', + '#default_value' => $type->name, + '#description' => t('The human-readable name of this content type. This text will be displayed as part of the list on the create content page. It is recommended that this name consists only of lowercase letters, numbers, and spaces. The name must be unique to this content type.'), + '#required' => TRUE, + ); + + if (!$type->locked) { + $form['identity']['type'] = array( + '#title' => t('Type'), + '#type' => 'textfield', + '#default_value' => $type->type, + '#maxlength' => 32, + '#required' => TRUE, + '#description' => t('The machine-readable name of this content type. This text will be used for constructing the URL of the create content page for this content type. It is recommended that this name consists only of lowercase letters, numbers, and underscores. Dashes are not allowed. Underscores will be converted into dashes when constructing the URL of the create content page. The name must be unique to this content type.'), + ); + } + else { + $form['identity']['type'] = array( + '#type' => 'value', + '#value' => $type->type, + ); + $form['identity']['type_display'] = array( + '#title' => t('Type'), + '#type' => 'item', + '#value' => theme('placeholder', $type->type), + '#description' => t('The machine-readable name of this content type. This field cannot be modified for system-defined content types.'), + ); + } + + $form['submission'] = array( + '#type' => 'fieldset', + '#title' =>t('Submission form'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + if ($type->has_title) { + $form['submission']['title_label'] = array( + '#title' => t('Title field label'), + '#type' => 'textfield', + '#default_value' => $type->title_label, + '#description' => t('The label for the title field of this content type.'), + '#required' => TRUE, + ); + } + if ($type->has_body) { + $form['submission']['body_label'] = array( + '#title' => t('Body field label'), + '#type' => 'textfield', + '#default_value' => $type->body_label, + '#description' => t('The label for the body field of this content type.'), + '#required' => TRUE, + ); + } + $form['submission']['description'] = array( + '#title' => t('Description'), + '#type' => 'textarea', + '#default_value' => $type->description, + '#description' => t('A brief description of this content type. This text will be displayed as part of the list on the create content page.'), + ); + $form['submission']['help'] = array( + '#type' => 'textarea', + '#title' => t('Explanation or submission guidelines'), + '#default_value' => $type->help, + '#description' => t('This text will be displayed at the top of the submission form for this content type. It is useful for helping or instructing your users.') + ); + $form['submission']['min_word_count'] = array( + '#type' => 'select', + '#title' => t('Minimum number of words'), + '#default_value' => $type->min_word_count, + '#options' => drupal_map_assoc(array(0, 10, 25, 50, 75, 100, 125, 150, 175, 200)), + '#description' => t('The minimum number of words for the body field to be considered valid for this content type. This can be useful to rule out submissions that do not meet the site\'s standards, such as short test posts.') + ); + $form['workflow'] = array( + '#type' => 'fieldset', + '#title' =>t('Workflow'), + '#collapsible' => TRUE, + ); + $form['workflow']['node_options'] = array('#type' => 'checkboxes', + '#title' => t('Default options'), + '#default_value' => variable_get('node_options_'. $type->type, array('status', 'promote')), + '#options' => array( + 'status' => t('Published'), + 'promote' => t('Promoted to front page'), + 'sticky' => t('Sticky at top of lists'), + 'revision' => t('Create new revision'), + ), + '#description' => t('Users with the administer nodes permission will be able to override these options.'), + ); + + $form['old_type'] = array( + '#type' => 'value', + '#value' => $type->type, + ); + $form['orig_type'] = array( + '#type' => 'value', + '#value' => $type->orig_type, + ); + $form['module'] = array( + '#type' => 'value', + '#value' => $type->module, + ); + $form['custom'] = array( + '#type' => 'value', + '#value' => $type->custom, + ); + $form['modified'] = array( + '#type' => 'value', + '#value' => $type->modified, + ); + $form['locked'] = array( + '#type' => 'value', + '#value' => $type->locked, + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save content type'), + ); + + if ($type->custom) { + if (!empty($type->type)) { + $form['delete'] = array( + '#type' => 'submit', + '#value' => t('Delete content type'), + ); + } + } + else { + $form['reset'] = array( + '#type' => 'submit', + '#value' => t('Reset to defaults'), + ); + } + + return drupal_get_form('node_type_form', $form); +} + +/** + * Implementation of hook_form_validate(). + */ +function node_type_form_validate($form_id, $form_values) { + $type = new stdClass(); + $type->type = trim($form_values['type']); + $type->name = trim($form_values['name']); + + // Work out what the type was before the user submitted this form + $old_type = trim($form_values['old_type']); + if (empty($old_type)) { + $old_type = $type->type; + } + + $types = node_get_types('names'); + + if (!$form_values['locked']) { + if (isset($types[$type->type]) && $type->type != $old_type) { + form_set_error('type', t('The machine-readable name %type is already taken.', array('%type' => theme('placeholder', $type->type)))); + } + if (strpos($type->type, '-') !== FALSE) { + form_set_error('type', t('The machine-readable name cannot contain dashes.', array('%type' => theme('placeholder', $type->type)))); + } + } + + $names = array_flip($types); + + if (isset($names[$type->name]) && $names[$type->name] != $old_type) { + form_set_error('name', t('The human-readable name %name is already taken.', array('%name' => theme('placeholder', $names[$type->name])))); + break; + } +} + +/** + * Implementation of hook_form_submit(). + */ +function node_type_form_submit($form_id, $form_values) { + $op = isset($_POST['op']) ? $_POST['op'] : ''; + + $type = new stdClass(); + + $type->type = trim($form_values['type']); + $type->name = trim($form_values['name']); + $type->orig_type = trim($form_values['orig_type']); + $type->old_type = isset($form_values['old_type']) ? $form_values['old_type'] : $type->type; + + $type->description = $form_values['description']; + $type->help = $form_values['help']; + $type->min_word_count = $form_values['min_word_count']; + $type->title_label = $form_values['title_label']; + $type->body_label = $form_values['body_label']; + + $type->module = !empty($form_values['module']) ? $form_values['module'] : 'node'; + $type->has_title = $type->has_body = TRUE; + $type->custom = $form_values['custom']; + $type->modified = TRUE; + $type->locked = $form_values['locked']; + + if ($op == t('Reset to defaults')) { + node_type_reset($type); + } + elseif ($op == t('Delete node type')) { + return 'admin/content/types/'. $type->old_type .'/delete'; + } + + if (!empty($form_values['old_type'])) { + if ($type->old_type != $type->type) { + $update_count = node_type_update_nodes($type->old_type, $type->type); + + if ($update_count) { + drupal_set_message(t('Changed the content type of %update_count posts from %old_type to %type.', array('%update_count' => theme('placeholder', $update_count), '%old_type' => theme('placeholder', $type->old_type), '%type' => theme('placeholder', $type->type)))); + cache_clear_all('filter:', TRUE); + } + } + } + + $status = node_type_save($type); + + // Remove everything that's been saved already - whatever's left is assumed + // to be a persistent variable. + foreach ($form_values as $key => $value) { + if (isset($type->$key)) { + unset($form_values[$key]); + } + } + + unset($form_values['type_display'], $form_values['old_type'], $form_values['orig_type'], $form_values['submit'], $form_values['delete'], $form_values['reset'], $form_values['form_id']); + + // Save or reset persistent variable values. + foreach ($form_values as $key => $value) { + $key .= '_'. $type->type; + if ($op == t('Reset to defaults')) { + variable_del($key); + } + else { + if (is_array($value)) { + $value = array_keys(array_filter($value)); + } + variable_set($key, $value); + + if ($type->old_type != $type->type) { + $key = str_replace($type->type, $type->old_type, $key); + variable_del($key); + } + } + } + + node_types_rebuild(); + menu_rebuild(); + cache_clear_all(); + $t_args = array('%name' => theme('placeholder', $type->name)); + + if ($op == t('Reset to defaults')) { + drupal_set_message(t('The content type %name has been reset to its default values.', $t_args)); + return; + } + + if ($status == SAVED_UPDATED) { + drupal_set_message(t('The content type %name has been updated.', $t_args)); + } + elseif ($status == SAVED_NEW) { + drupal_set_message(t('The content type %name has been added.', $t_args)); + watchdog('node', t('Added content type %name.', $t_args), WATCHDOG_NOTICE, l(t('view'), 'admin/content/types')); + } + + return 'admin/content/types'; +} + +/** + * Resets all of the relevant fields of a module-defined node type to their + * default values. + * + * @param &$type + * The node type to reset. The node type is passed back by reference with its + * resetted values. If there is no module-defined info for this node type, + * then nothing happens. + */ +function node_type_reset(&$type) { + $info_array = module_invoke($type->module, 'node_info'); + if (isset($info_array[$type->orig_type])) { + $info = _node_type_set_defaults($info_array[$type->orig_type]); + $info['type'] = $type->orig_type; + + foreach ($info as $field => $value) { + $type->$field = $value; + } + } +} + +/** + * Menu callback; delete a single content type. + */ +function node_type_delete($type) { + $form['type'] = array('#type' => 'value', '#value' => $type->type); + $form['name'] = array('#type' => 'value', '#value' => $type->name); + + $message = t('Are you sure you want to delete the content type %type?', array('%type' => theme('placeholder', $type->name))); + + return confirm_form('node_type_delete_form', $form, $message, 'admin/content/types', t('This action cannot be undone.'), t('Delete')); +} + +/** + * Process content type delete form submissions. + */ +function node_type_delete_form_submit($form_id, $form_values) { + db_query("DELETE FROM {node_type} WHERE type = '%s'", $form_values['type']); + + $t_args = array('%name' => theme('placeholder', $form_values['name'])); + drupal_set_message(t('The content type %name has been deleted.', $t_args)); + watchdog('menu', t('Deleted content type %name.', $t_args), WATCHDOG_NOTICE); + + node_types_rebuild(); + menu_rebuild(); + + return 'admin/content/types'; +} diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/node/node.module ./modules/node/node.module --- ../C-HEAD/modules/node/node.module 2006-08-02 16:07:13.000000000 -0700 +++ ./modules/node/node.module 2006-08-02 15:56:23.000000000 -0700 @@ -36,6 +36,10 @@ function node_help($section) { return t('Allows content to be submitted to the site and displayed on pages.'); case 'admin/content/search': return t('

    Enter a simple pattern to search for a post. This can include the wildcard character *.
    For example, a search for "br*" might return "bread bakers", "our daily bread" and "brenda".

    '); + case 'admin/content/types': + return '

    '. t('Below is a list of all the content types on your site. All posts that exist on your site are instances of one of these content types.') .'

    '; + case 'admin/content/types/add': + return '

    '. t('To create a new content type, enter the human-readable name, the machine-readable name, and all other relevant fields that are on this page. Once created, users of your site will be able to create posts that are instances of this content type.'); } if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) == 'revisions') { @@ -43,7 +47,8 @@ function node_help($section) { } if (arg(0) == 'node' && arg(1) == 'add' && $type = arg(2)) { - return filter_xss_admin(variable_get($type .'_help', '')); + $type = node_get_types('type', arg(2)); + return filter_xss_admin($type->help); } } @@ -190,16 +195,32 @@ function node_teaser($body, $format = NU return truncate_utf8($body, $size); } -function _node_names($op = '', $node = NULL) { - static $node_names = array(); - static $node_list = array(); - - if (empty($node_names)) { - $node_names = module_invoke_all('node_info'); - foreach ($node_names as $type => $value) { - $node_list[$type] = $value['name']; - } +/** + * Builds a list of available node types, and returns all of part of this list + * in the specified format. + * + * @param $op + * The format in which to return the list. When this is set to 'type', + * 'module', or 'name', only the specified node type is returned. When set to + * 'types' or 'names', all node types are returned. + * @param $node + * A node object, array, or string that indicates the node type to return. + * Leave at default value (NULL) to return a list of all node types. + * @param $reset + * Whether or not to reset this function's internal cache (defaults to + * FALSE). + * + * @return + * Either an array of all available node types, or a single node type, in a + * variable format. + */ +function node_get_types($op = 'types', $node = NULL, $reset = FALSE) { + static $_node_types, $_node_names; + + if ($reset || !isset($_node_types)) { + list($_node_types, $_node_names) = _node_types_build(); } + if ($node) { if (is_array($node)) { $type = $node['type']; @@ -210,54 +231,147 @@ function _node_names($op = '', $node = N elseif (is_string($node)) { $type = $node; } - if (!isset($node_names[$type])) { + if (!isset($_node_types[$type])) { return FALSE; } } switch ($op) { - case 'base': - return $node_names[$type]['base']; - case 'list': - return $node_list; + case 'types': + return $_node_types; + case 'type': + return $_node_types[$type]; + case 'module': + return $_node_types[$type]->module; + case 'names': + return $_node_names; case 'name': - return $node_list[$type]; + return $_node_names[$type]; + } +} + +/** + * Resets the database cache of node types, and saves all new or non-modified + * module-defined node types to the database. + */ +function node_types_rebuild() { + _node_types_build(); + + $node_types = node_get_types('types', NULL, TRUE); + + foreach ($node_types as $type => $info) { + if (!empty($info->is_new)) { + node_type_save($info); + } } + + _node_types_build(); } /** - * Determine the basename for hook_load etc. + * Saves a node type to the database. + * + * @param $info + * The node type to save, as an object. * - * @param $node - * Either a node object, a node array, or a string containing the node type. * @return - * The basename for hook_load, hook_nodeapi etc. + * Status flag indicating outcome of the operation. */ -function node_get_base($node) { - return _node_names('base', $node); +function node_type_save($info) { + $is_existing = FALSE; + $existing_type = !empty($info->old_type) ? $info->old_type : $info->type; + $is_existing = db_num_rows(db_query("SELECT * FROM {node_type} WHERE type = '%s'", $existing_type)); + + if ($is_existing) { + db_query("UPDATE {node_type} SET type = '%s', name = '%s', module = '%s', has_title = %d, title_label = '%s', has_body = %d, body_label = '%s', description = '%s', help = '%s', min_word_count = %d, custom = %d, modified = %d, locked = %d WHERE type = '%s'", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $existing_type); + return SAVED_UPDATED; + } + else { + db_query("INSERT INTO {node_type} (type, name, module, has_title, title_label, has_body, body_label, description, help, min_word_count, custom, modified, locked, orig_type) VALUES ('%s', '%s', '%s', %d, '%s', %d, '%s', '%s', '%s', %d, %d, %d, %d, '%s')", $info->type, $info->name, $info->module, $info->has_title, $info->title_label, $info->has_body, $info->body_label, $info->description, $info->help, $info->min_word_count, $info->custom, $info->modified, $info->locked, $info->orig_type); + return SAVED_NEW; + } } /** - * Determine the human readable name for a given type. + * Updates all nodes of one type to be of another type. + * + * @param $orig_type + * The current node type of the nodes. + * @param $type + * The new node type of the nodes. * - * @param $node - * Either a node object, a node array, or a string containing the node type. * @return - * The human readable name of the node type. + * The number of nodes whose node type field was modified. */ -function node_get_name($node) { - return _node_names('name', $node); +function node_type_update_nodes($old_type, $type) { + return db_num_rows(db_query("UPDATE {node} SET type = '%s' WHERE type = '%s'", $type, $old_type)); } /** - * Return the list of available node types. + * Builds the list of available node types, by querying hook_node_info() in all + * modules, and by looking for node types in the database. * - * @param $node - * Either a node object, a node array, or a string containing the node type. - * @return - * An array consisting ('#type' => name) pairs. */ -function node_get_types() { - return _node_names('list'); +function _node_types_build() { + $_node_types = array(); + $_node_names = array(); + + $info_array = module_invoke_all('node_info'); + foreach ($info_array as $type => $info) { + $info['type'] = $type; + $_node_types[$type] = (object) _node_type_set_defaults($info); + $_node_names[$type] = $info['name']; + } + + $type_result = db_query(db_rewrite_sql('SELECT nt.type, nt.* FROM {node_type} nt ORDER BY nt.type ASC', 'nt', 'type')); + while ($type_object = db_fetch_object($type_result)) { + if (!isset($_node_types[$type_object->type]) || $type_object->modified) { + $_node_types[$type_object->type] = $type_object; + $_node_names[$type_object->type] = $type_object->name; + + if ($type_object->type != $type_object->orig_type) { + unset($_node_types[$type_object->orig_type]); + unset($_node_names[$type_object->orig_type]); + } + } + } + + asort($_node_names); + + return array($_node_types, $_node_names); +} + +/** + * Set default values for a node type defined through hook_node_info(). + */ +function _node_type_set_defaults($info) { + if (!isset($info['has_title'])) { + $info['has_title'] = TRUE; + } + if ($info['has_title'] && !isset($info['title_label'])) { + $info['title_label'] = t('Title'); + } + + if (!isset($info['has_body'])) { + $info['has_body'] = TRUE; + } + if ($info['has_body'] && !isset($info['body_label'])) { + $info['body_label'] = t('Body'); + } + + if (!isset($info['custom'])) { + $info['custom'] = FALSE; + } + if (!isset($info['modified'])) { + $info['modified'] = FALSE; + } + if (!isset($info['locked'])) { + $info['locked'] = TRUE; + } + + $info['orig_type'] = $info['type']; + $info['is_new'] = TRUE; + + return $info; } /** @@ -271,7 +385,11 @@ function node_get_types() { * TRUE iff the $hook exists in the node type of $node. */ function node_hook(&$node, $hook) { - return module_hook(node_get_base($node), $hook); + $module = node_get_types('module', $node); + if ($module == 'node') { + $module = 'node_content'; // Avoid function name collisions. + } + return module_hook($module, $hook); } /** @@ -288,7 +406,11 @@ function node_hook(&$node, $hook) { */ function node_invoke(&$node, $hook, $a2 = NULL, $a3 = NULL, $a4 = NULL) { if (node_hook($node, $hook)) { - $function = node_get_base($node) ."_$hook"; + $module = node_get_types('module', $node); + if ($module == 'node') { + $module = 'node_content'; // Avoid function name collisions. + } + $function = $module .'_'. $hook; return ($function($node, $a2, $a3, $a4)); } } @@ -581,7 +703,18 @@ function node_show($node, $cid) { * Implementation of hook_perm(). */ function node_perm() { - return array('administer nodes', 'access content', 'view revisions', 'revert revisions'); + $perms = array('administer content types', 'administer nodes', 'access content', 'view revisions', 'revert revisions'); + + foreach (node_get_types() as $type) { + if ($type->module == 'node') { + $name = check_plain($type->name); + $perms[] = 'create '. $name .' content'; + $perms[] = 'edit own '. $name .' content'; + $perms[] = 'edit '. $name .' content'; + } + } + + return $perms; } /** @@ -720,7 +853,7 @@ function node_search($op = 'search', $ke $extra = node_invoke_nodeapi($node, 'search result'); $results[] = array('link' => url('node/'. $item->sid), - 'type' => node_get_name($node), + 'type' => node_get_types('name', $node), 'title' => $node->title, 'user' => theme('username', $node), 'date' => $node->changed, @@ -861,12 +994,25 @@ function node_menu($may_cache) { 'callback' => 'node_configure', 'access' => user_access('administer nodes') ); + $items[] = array( 'path' => 'admin/content/types', 'title' => t('content types'), 'description' => t('Manage posts by content type, including default status, front page promotion, etc.'), - 'callback' => 'node_types_configure', - 'access' => user_access('administer nodes') + 'callback' => 'node_overview_types', + 'access' => user_access('administer nodes'), + ); + $items[] = array( + 'path' => 'admin/content/types/list', + 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items[] = array( + 'path' => 'admin/content/types/add', + 'title' => t('add content type'), + 'callback' => 'node_type_form', + 'type' => MENU_LOCAL_TASK, ); $items[] = array('path' => 'node', 'title' => t('content'), @@ -882,6 +1028,18 @@ function node_menu($may_cache) { 'callback' => 'node_feed', 'access' => user_access('access content'), 'type' => MENU_CALLBACK); + + foreach (node_get_types() as $type) { + if (module_exist($type->module)) { + $name = check_plain($type->name); + $type_url_str = str_replace('_', '-', $type->type); + $items[] = array( + 'path' => 'node/add/'. $type_url_str, + 'title' => t($name), + 'access' => node_access('create', $type->type), + ); + } + } } else { if (arg(0) == 'node' && is_numeric(arg(1))) { @@ -911,10 +1069,36 @@ function node_menu($may_cache) { 'type' => MENU_LOCAL_TASK); } } - else if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'types' && is_string(arg(3))) { - $items[] = array('path' => 'admin/content/types/'. arg(3), - 'title' => t("'%name' content type", array('%name' => node_get_name(arg(3)))), - 'type' => MENU_CALLBACK); + + // Content type configuration. + if (arg(0) == 'admin' && arg(1) == 'content' && arg(2) == 'types') { + include_once './'. drupal_get_path('module', 'node') .'/content_types.inc'; + + if (arg(3) != NULL) { + $type_name = arg(3); + $type_name = !empty($type_name) ? str_replace('-', '_', $type_name) : NULL; + $type = node_get_types('type', $type_name); + + if (!empty($type)) { + $type->name = check_plain($type->name); + $type_url_str = str_replace('_', '-', $type->type); + + $items[] = array( + 'path' => 'admin/content/types/'. $type_url_str, + 'title' => t($type->name), + 'callback' => 'node_type_form', + 'callback arguments' => array($type), + 'type' => MENU_CALLBACK, + ); + $items[] = array( + 'path' => 'admin/content/types/'. $type_url_str .'/delete', + 'title' => t('delete'), + 'callback' => 'node_type_delete', + 'callback arguments' => array($type), + 'type' => MENU_CALLBACK, + ); + } + } } } @@ -985,7 +1169,7 @@ function node_filters() { 'options' => array('status-1' => t('published'), 'status-0' => t('not published'), 'promote-1' => t('promoted'), 'promote-0' => t('not promoted'), 'sticky-1' => t('sticky'), 'sticky-0' => t('not sticky'))); - $filters['type'] = array('title' => t('type'), 'options' => node_get_types()); + $filters['type'] = array('title' => t('type'), 'options' => node_get_types('names')); // The taxonomy filter if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) { $filters['category'] = array('title' => t('category'), 'options' => $taxonomy); @@ -1198,7 +1382,7 @@ function node_admin_nodes() { while ($node = db_fetch_object($result)) { $nodes[$node->nid] = ''; $form['title'][$node->nid] = array('#value' => l($node->title, 'node/'. $node->nid) .' '. theme('mark', node_mark($node->nid, $node->changed))); - $form['name'][$node->nid] = array('#value' => node_get_name($node)); + $form['name'][$node->nid] = array('#value' => node_get_types('name', $node)); $form['username'][$node->nid] = array('#value' => theme('username', $node)); $form['status'][$node->nid] = array('#value' => ($node->status ? t('published') : t('not published'))); $form['operations'][$node->nid] = array('#value' => l(t('edit'), 'node/'. $node->nid .'/edit', array(), $destination)); @@ -1275,40 +1459,6 @@ function node_multiple_delete_confirm_su } /** - * Menu callback; presents each node type configuration page. - */ -function node_types_configure($type = NULL) { - if (isset($type)) { - $node = new stdClass(); - $node->type = $type; - $form['submission'] = array('#type' => 'fieldset', '#title' =>t('Submission form') ); - $form['submission'][$type . '_help'] = array( - '#type' => 'textarea', '#title' => t('Explanation or submission guidelines'), '#default_value' => variable_get($type .'_help', ''), - '#description' => t('This text will be displayed at the top of the %type submission form. It is useful for helping or instructing your users.', array('%type' => node_get_name($type))) - ); - $form['submission']['minimum_'. $type .'_size'] = array( - '#type' => 'select', '#title' => t('Minimum number of words'), '#default_value' => variable_get('minimum_'. $type .'_size', 0), '#options' => drupal_map_assoc(array(0, 10, 25, 50, 75, 100, 125, 150, 175, 200)), - '#description' => t('The minimum number of words a %type must be to be considered valid. This can be useful to rule out submissions that do not meet the site\'s standards, such as short test posts.', array('%type' => node_get_name($type))) - ); - $form['workflow'] = array('#type' => 'fieldset', '#title' =>t('Workflow')); - $form['type'] = array('#type' => 'value', '#value' => $type); - - $form['array_filter'] = array('#type' => 'value', '#value' => TRUE); - return system_settings_form($type .'_node_settings', $form); - } - else { - $header = array(t('Type'), t('Operations')); - - $rows = array(); - foreach (node_get_types() as $type => $name) { - $rows[] = array($name, l(t('configure'), 'admin/content/types/'. $type)); - } - - return theme('table', $header, $rows); - } -} - -/** * Generate an overview table of older revisions of a node. */ function node_revision_overview($node) { @@ -1587,7 +1737,7 @@ function node_validate($node, $form = ar // Make sure the body has the minimum number of words. // todo use a better word counting algorithm that will work in other languages if (isset($node->body) && count(explode(' ', $node->body)) < variable_get('minimum_'. $node->type .'_size', 0)) { - form_set_error('body', t('The body of your %type is too short. You need at least %words words.', array('%words' => variable_get('minimum_'. $node->type .'_size', 0), '%type' => node_get_name($node)))); + form_set_error('body', t('The body of your %type is too short. You need at least %words words.', array('%words' => variable_get('minimum_'. $node->type .'_size', 0), '%type' => node_get_types('name', $node)))); } if (isset($node->nid) && (node_last_changed($node->nid) > $_POST['edit']['changed'])) { @@ -1643,8 +1793,8 @@ function node_form($node) { } /** -* Generate the node editing form array. -*/ + * Generate the node editing form array. + */ function node_form_array($node) { node_object_prepare($node); @@ -1783,21 +1933,25 @@ function theme_node_form($form) { function node_add($type) { global $user; + $types = node_get_types(); + $type = isset($type) ? str_replace('-', '_', $type) : NULL; // If a node type has been specified, validate its existence. - if (array_key_exists($type, node_get_types()) && node_access('create', $type)) { + if (isset($types[$type]) && node_access('create', $type)) { // Initialize settings: $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => $type); $output = node_form($node); - drupal_set_title(t('Submit %name', array('%name' => node_get_name($node)))); + drupal_set_title(t('Submit %name', array('%name' => check_plain($types[$type]->name)))); } else { // If no (valid) node type has been provided, display a node type overview. - foreach (node_get_types() as $type => $name) { - if (node_access('create', $type)) { - $out = '

    '. l($name, "node/add/$type", array('title' => t('Add a new %s.', array('%s' => $name)))) .'
    '; - $out .= '
    '. implode("\n", module_invoke_all('help', 'node/add#'. $type)) .'
    '; - $item[$name] = $out; + foreach ($types as $type) { + if (module_exist($type->module) && node_access('create', $type->type)) { + $type_url_str = str_replace('_', '-', $type->type); + $title = t('Add a new %s.', array('%s' => check_plain($type->name))); + $out = '
    '. l($type->name, "node/add/$type_url_str", array('title' => $title)) .'
    '; + $out .= '
    '. filter_xss_admin($type->description) .'
    '; + $item[$type->type] = $out; } } @@ -1806,7 +1960,7 @@ function node_add($type) { $output = t('Choose the appropriate item from the list:') .'
    '. implode('', $item) .'
    '; } else { - $output = t('You are not allowed to create content.'); + $output = t('No content types available.'); } } @@ -1854,7 +2008,7 @@ function node_preview($node) { $output = theme('node_preview', $cloned_node); } drupal_set_title(t('Preview')); - drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('create content'), 'node/add'), l(t('Submit %name', array('%name' => node_get_name($node))), 'node/add/'. $node->type))); + drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('create content'), 'node/add'), l(t('Submit %name', array('%name' => node_get_types('name', $node))), 'node/add/'. $node->type))); return $output; } @@ -1896,7 +2050,7 @@ function node_form_submit($form_id, $edi if (node_access('update', $node)) { node_save($node); watchdog('content', t('%type: updated %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid)); - drupal_set_message(t('The %post was updated.', array ('%post' => node_get_name($node)))); + drupal_set_message(t('The %post was updated.', array ('%post' => node_get_types('name', $node)))); } } else { @@ -1905,7 +2059,7 @@ function node_form_submit($form_id, $edi if (node_access('create', $node)) { node_save($node); watchdog('content', t('%type: added %title.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title))), WATCHDOG_NOTICE, l(t('view'), "node/$node->nid")); - drupal_set_message(t('Your %post was created.', array ('%post' => node_get_name($node)))); + drupal_set_message(t('Your %post was created.', array ('%post' => node_get_types('name', $node)))); } } if ($node->nid) { @@ -2189,23 +2343,8 @@ function node_update_index() { * Implementation of hook_form_alter(). */ function node_form_alter($form_id, &$form) { - // Node publishing options - if (isset($form['type']) && $form['type']['#value'] .'_node_settings' == $form_id) { - $form['workflow']['node_options_'. $form['type']['#value']] = array('#type' => 'checkboxes', - '#title' => t('Default options'), - '#default_value' => variable_get('node_options_'. $form['type']['#value'], array('status', 'promote')), - '#options' => array( - 'status' => t('Published'), - 'promote' => t('Promoted to front page'), - 'sticky' => t('Sticky at top of lists'), - 'revision' => t('Create new revision'), - ), - '#description' => t('Users with the administer nodes permission will be able to override these options.'), - ); - } - // Advanced node search form - elseif ($form_id == 'search_form' && arg(1) == 'node' && user_access('use advanced search')) { + if ($form_id == 'search_form' && arg(1) == 'node' && user_access('use advanced search')) { // Keyword boxes: $form['advanced'] = array( '#type' => 'fieldset', @@ -2378,7 +2517,11 @@ function node_access($op, $node = NULL, // Can't use node_invoke(), because the access hook takes the $op parameter // before the $node parameter. - $access = module_invoke(node_get_base($node), 'access', $op, $node); + $module = node_get_types('module', $node); + if ($module == 'node') { + $module = 'node_content'; // Avoid function name collisions. + } + $access = module_invoke($module, 'access', $op, $node); if (!is_null($access)) { return $access; } @@ -2529,3 +2672,58 @@ function node_db_rewrite_sql($query, $pr */ +/** + * @defgroup node_content Hook implementations for user-created content types. + * @{ + */ + +/** + * Implementation of hook_access(). + */ +function node_content_access($op, $node) { + global $user; + $type = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type); + + if ($op == 'create') { + return user_access('create '. $type .' content'); + } + + if ($op == 'update' || $op == 'delete') { + if (user_access('edit '. $type .' content') || (user_access('edit own '. $type .' content') && ($user->uid == $node->uid))) { + return TRUE; + } + } +} + +/** + * Implementation of hook_form(). + */ +function node_content_form($node) { + $type = node_get_types('type', $node); + + if ($type->has_title) { + $form['title'] = array( + '#type' => 'textfield', + '#title' => check_plain($type->title_label), + '#required' => TRUE, + '#default_value' => $node->title, + '#weight' => -5, + ); + } + + if ($type->has_body) { + $form['body_filter']['body'] = array( + '#type' => 'textarea', + '#title' => check_plain($type->body_label), + '#default_value' => $node->body, + '#rows' => 20, + '#required' => TRUE); + $form['body_filter']['format'] = filter_form($node->format); + } + + return $form; +} + +/** + * @} End of "defgroup node_content". + */ diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/poll/poll.module ./modules/poll/poll.module --- ../C-HEAD/modules/poll/poll.module 2006-07-31 13:14:29.000000000 -0700 +++ ./modules/poll/poll.module 2006-07-31 23:44:36.000000000 -0700 @@ -25,8 +25,6 @@ function poll_help($section) { return $output; case 'admin/settings/modules#description': return t("Allows your site to capture votes on different topics in the form of multiple choice questions."); - case 'node/add#poll': - return t("A poll is a multiple-choice question which visitors can vote on."); } } @@ -128,8 +126,9 @@ function poll_validate($node) { */ function poll_form(&$node) { $admin = user_access('administer nodes'); + $type = node_get_types('type', $node); - $form['title'] = array('#type' => 'textfield', '#title' => t('Question'), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -1); + $form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => $node->title, '#weight' => -1); $form['choice']['choices'] = array('#type' => 'hidden', '#default_value' => max(2, count($node->choice) ? count($node->choice) : 5)); $form['choice']['morechoices'] = array('#type' => 'checkbox', '#title' => t('Need more choices'), '#default_value' => 0, '#description' => t("If the amount of boxes above isn't enough, check this box and click the Preview button below to add some more."), '#weight' => 1); @@ -271,7 +270,15 @@ function poll_load($node) { * Implementation of hook_node_info(). */ function poll_node_info() { - return array('poll' => array('name' => t("poll"), 'base' => 'poll')); + return array( + 'poll' => array( + 'name' => t("poll"), + 'module' => 'poll', + 'description' => t("A poll is a multiple-choice question which visitors can vote on."), + 'title_label' => t('Question'), + 'has_body' => FALSE, + ) + ); } function poll_page() { diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/system/system.install ./modules/system/system.install --- ../C-HEAD/modules/system/system.install 2006-07-26 00:19:41.000000000 -0700 +++ ./modules/system/system.install 2006-07-30 05:06:47.000000000 -0700 @@ -198,6 +198,24 @@ function system_install() { KEY uid (uid) ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {node_type} ( + type varchar(32) NOT NULL, + name varchar(255) NOT NULL default '', + module varchar(255) NOT NULL, + description mediumtext NOT NULL, + help mediumtext NOT NULL, + has_title tinyint(3) unsigned NOT NULL, + title_label varchar(255) NOT NULL default '', + has_body tinyint(3) unsigned NOT NULL, + body_label varchar(255) NOT NULL default '', + min_word_count smallint(4) unsigned NOT NULL, + custom tinyint(1) NOT NULL DEFAULT '0', + modified tinyint(1) NOT NULL DEFAULT '0', + locked tinyint(1) NOT NULL DEFAULT '0', + orig_type varchar(255) NOT NULL default '', + PRIMARY KEY (type) + ) /*!40100 DEFAULT CHARACTER SET UTF8 */ "); + db_query("CREATE TABLE {url_alias} ( pid int(10) unsigned NOT NULL auto_increment, src varchar(128) NOT NULL default '', @@ -405,6 +423,9 @@ function system_install() { db_query("INSERT INTO {node_access} VALUES (0, 0, 'all', 1, 0, 0)"); + db_query("INSERT INTO {node_type} (type, name, module, description, help, has_title, title_label, has_body, body_label, min_word_count, custom, modified, locked, orig_type) VALUES ('page', 'page', 'node', 'If you want to add a static page, like a contact page or an about page, use a page.', '', 1, 'Title', 1, 'Body', 0, 1, 1, 0, 'page')"); + db_query("INSERT INTO {node_type} (type, name, module, description, help, has_title, title_label, has_body, body_label, min_word_count, custom, modified, locked, orig_type) VALUES ('story', 'story', 'node', 'Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extended by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.', '', 1, 'Title', 1, 'Body', 0, 1, 1, 0, 'story')"); + db_query("INSERT INTO {filter_formats} (name, roles, cache) VALUES ('Filtered HTML',',1,2,',1)"); db_query("INSERT INTO {filter_formats} (name, roles, cache) VALUES ('PHP code','',0)"); db_query("INSERT INTO {filter_formats} (name, roles, cache) VALUES ('Full HTML','',1)"); @@ -2524,3 +2545,99 @@ function system_update_187() { } return $ret; } + +function system_update_188() { + // Add ability to create dynamic node types like the CCK module + $ret = array(); + + switch ($GLOBALS['db_type']) { + case 'mysqli': + case 'mysql': + // Create node_type table + $ret[] = update_sql("CREATE TABLE {node_type} ( + type varchar(32) NOT NULL, + name varchar(255) NOT NULL, + module varchar(255) NOT NULL, + description mediumtext NOT NULL, + help mediumtext NOT NULL, + has_title tinyint(3) unsigned NOT NULL, + title_label varchar(255) NOT NULL default '', + has_body tinyint(3) unsigned NOT NULL, + body_label varchar(255) NOT NULL default '', + min_word_count smallint(4) unsigned NOT NULL, + custom tinyint(1) NOT NULL DEFAULT '0', + modified tinyint(1) NOT NULL DEFAULT '0', + locked tinyint(1) NOT NULL DEFAULT '0', + orig_type varchar(255) NOT NULL default '', + PRIMARY KEY (type) + ) /*!40100 DEFAULT CHARACTER SET utf8 */;"); + break; + + case 'pgsql': + $ret[] = update_sql("CREATE TABLE {node_type} ( + type varchar(32) NOT NULL, + name varchar(255) NOT NULL, + module varchar(255) NOT NULL, + description text NOT NULL, + help text NOT NULL, + has_title integer unsigned NOT NULL, + title_label varchar(255) NOT NULL default '', + has_body integer unsigned NOT NULL, + body_label varchar(255) NOT NULL default '', + min_word_count integer unsigned NOT NULL, + custom smallint NOT NULL DEFAULT '0', + modified smallint NOT NULL DEFAULT '0', + locked smallint NOT NULL DEFAULT '0', + orig_type varchar(255) NOT NULL default '', + PRIMARY KEY (type) + );"); + break; + } + + // Insert default user-defined node types into the database. + $types = array( + array( + 'type' => 'page', + 'name' => t('page'), + 'module' => 'node', + 'description' => t('If you want to add a static page, like a contact page or an about page, use a page.'), + 'custom' => TRUE, + 'modified' => TRUE, + 'locked' => FALSE, + ), + array( + 'type' => 'story', + 'name' => t('story'), + 'module' => 'node', + 'description' => t('Stories are articles in their simplest form: they have a title, a teaser and a body, but can be extendd by other modules. The teaser is part of the body too. Stories may be used as a personal blog or for news articles.'), + 'custom' => TRUE, + 'modified' => TRUE, + 'locked' => FALSE, + ) + ); + + foreach ($types as $type) { + $type = (object) _node_type_set_defaults($type); + node_type_save($type); + } + + cache_clear_all(); + system_modules(); + menu_rebuild(); + node_types_rebuild(); + + // Migrate old values for 'minimum_x_size' variables to the node_type table. + $query = db_query('SELECT type FROM {node_type}'); + while ($result = db_fetch_object($query)) { + $variable_name = 'minimum_'. $result->type .'_size'; + if ($value = db_fetch_object(db_query("SELECT value FROM {variable} WHERE name = '%s'", $variable_name))) { + $value = (int) unserialize($value->value); + db_query("UPDATE {node_type} SET min_word_count = %d, modified = %d WHERE type = '%s'", $value, 1, $result->type); + variable_del($variable_name); + } + } + + node_types_rebuild(); + + return $ret; +} diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/system/system.module ./modules/system/system.module --- ../C-HEAD/modules/system/system.module 2006-07-31 13:14:30.000000000 -0700 +++ ./modules/system/system.module 2006-07-31 23:44:37.000000000 -0700 @@ -1279,6 +1279,7 @@ function system_modules_submit($form_id, } menu_rebuild(); + node_types_rebuild(); drupal_set_message(t('The configuration options have been saved.')); return 'admin/settings/modules'; @@ -1403,7 +1404,7 @@ function system_theme_settings($key = '' ); // Toggle node display. - $node_types = module_invoke('node', 'get_types'); + $node_types = node_get_types('names'); if ($node_types) { $form['node_info'] = array( '#type' => 'fieldset', diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/taxonomy/.#taxonomy.module.1.296 ./modules/taxonomy/.#taxonomy.module.1.296 --- ../C-HEAD/modules/taxonomy/.#taxonomy.module.1.296 1969-12-31 16:00:00.000000000 -0800 +++ ./modules/taxonomy/.#taxonomy.module.1.296 2006-07-31 23:44:38.000000000 -0700 @@ -0,0 +1,1396 @@ +links(taxonomy_link('taxonomy terms', $node)); + * } + */ +function taxonomy_link($type, $node = NULL) { + if ($type == 'taxonomy terms' && $node != NULL) { + $links = array(); + if (array_key_exists('taxonomy', $node)) { + foreach ($node->taxonomy as $term) { + $links['taxonomy_term_'. $term->tid] = array( + 'title' => $term->name, + 'href' => taxonomy_term_path($term), + 'attributes' => array('rel' => 'tag', 'title' => strip_tags($term->description)) + ); + } + } + + // We call this hook again because some modules and themes call taxonomy_link('taxonomy terms') directly + foreach (module_implements('link_alter') AS $module) { + $function = $module .'_link_alter'; + $function($node, $links); + } + + return $links; + } +} + +function taxonomy_term_path($term) { + $vocabulary = taxonomy_get_vocabulary($term->vid); + if ($vocabulary->module != 'taxonomy' && $path = module_invoke($vocabulary->module, 'term_path', $term)) { + return $path; + } + return 'taxonomy/term/'. $term->tid; +} + +/** + * Implementation of hook_menu(). + */ +function taxonomy_menu($may_cache) { + $items = array(); + + if ($may_cache) { + $items[] = array('path' => 'admin/content/taxonomy', + 'title' => t('categories'), + 'description' => t('Create vocabularies and terms to categorize your content.'), + 'callback' => 'taxonomy_overview_vocabularies', + 'access' => user_access('administer taxonomy')); + + $items[] = array('path' => 'admin/content/taxonomy/list', + 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10); + + $items[] = array('path' => 'admin/content/taxonomy/add/vocabulary', + 'title' => t('add vocabulary'), + 'callback' => 'taxonomy_admin_vocabulary_edit', + 'access' => user_access('administer taxonomy'), + 'type' => MENU_LOCAL_TASK); + + $items[] = array('path' => 'admin/content/taxonomy/edit/vocabulary', + 'title' => t('edit vocabulary'), + 'callback' => 'taxonomy_admin_vocabulary_edit', + 'access' => user_access('administer taxonomy'), + 'type' => MENU_CALLBACK); + + $items[] = array('path' => 'admin/content/taxonomy/edit/term', + 'title' => t('edit term'), + 'callback' => 'taxonomy_admin_term_edit', + 'access' => user_access('administer taxonomy'), + 'type' => MENU_CALLBACK); + + $items[] = array('path' => 'taxonomy/term', + 'title' => t('taxonomy term'), + 'callback' => 'taxonomy_term_page', + 'access' => user_access('access content'), + 'type' => MENU_CALLBACK); + + $items[] = array('path' => 'taxonomy/autocomplete', + 'title' => t('autocomplete taxonomy'), + 'callback' => 'taxonomy_autocomplete', + 'access' => user_access('access content'), + 'type' => MENU_CALLBACK); + } + else { + if (is_numeric(arg(2))) { + $items[] = array('path' => 'admin/content/taxonomy/' . arg(2), + 'title' => t('list terms'), + 'callback' => 'taxonomy_overview_terms', + 'callback arguments' => array(arg(2)), + 'access' => user_access('administer taxonomy'), + 'type' => MENU_CALLBACK); + + $items[] = array('path' => 'admin/content/taxonomy/' . arg(2) . '/list', + 'title' => t('list'), + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10); + + $items[] = array('path' => 'admin/content/taxonomy/' . arg(2) . '/add/term', + 'title' => t('add term'), + 'callback' => 'taxonomy_form_term', + 'callback arguments' => array(array('vid' => arg(2))), + 'access' => user_access('administer taxonomy'), + 'type' => MENU_LOCAL_TASK); + } + } + + return $items; +} + +/** + * List and manage vocabularies. + */ +function taxonomy_overview_vocabularies() { + $vocabularies = taxonomy_get_vocabularies(); + $rows = array(); + foreach ($vocabularies as $vocabulary) { + $types = array(); + foreach ($vocabulary->nodes as $type) { + $node_type = node_get_types('name', $type); + $types[] = $node_type ? $node_type : $type; + } + $rows[] = array('name' => check_plain($vocabulary->name), + 'type' => implode(', ', $types), + 'edit' => l(t('edit vocabulary'), "admin/content/taxonomy/edit/vocabulary/$vocabulary->vid"), + 'list' => l(t('list terms'), "admin/content/taxonomy/$vocabulary->vid"), + 'add' => l(t('add terms'), "admin/content/taxonomy/$vocabulary->vid/add/term") + ); + } + if (empty($rows)) { + $rows[] = array(array('data' => t('No categories available.'), 'colspan' => '5', 'class' => 'message')); + } + $header = array(t('Name'), t('Type'), array('data' => t('Operations'), 'colspan' => '3')); + + return theme('table', $header, $rows, array('id' => 'taxonomy')); +} + +/** + * Display a tree of all the terms in a vocabulary, with options to edit + * each one. + */ +function taxonomy_overview_terms($vid) { + $destination = drupal_get_destination(); + + $header = array(t('Name'), t('Operations')); + $vocabulary = taxonomy_get_vocabulary($vid); + + drupal_set_title(check_plain($vocabulary->name)); + $start_from = $_GET['page'] ? $_GET['page'] : 0; + $total_entries = 0; // total count for pager + $page_increment = 25; // number of tids per page + $displayed_count = 0; // number of tids shown + + $tree = taxonomy_get_tree($vocabulary->vid); + foreach ($tree as $term) { + $total_entries++; // we're counting all-totals, not displayed + if (($start_from && ($start_from * $page_increment) >= $total_entries) || ($displayed_count == $page_increment)) { continue; } + $rows[] = array(_taxonomy_depth($term->depth) . ' ' . l($term->name, "taxonomy/term/$term->tid"), l(t('edit'), "admin/content/taxonomy/edit/term/$term->tid", array(), $destination)); + $displayed_count++; // we're counting tids displayed + } + + if (!$rows) { + $rows[] = array(array('data' => t('No terms available.'), 'colspan' => '2')); + } + + $GLOBALS['pager_page_array'][] = $start_from; // FIXME + $GLOBALS['pager_total'][] = intval($total_entries / $page_increment) + 1; // FIXME + + if ($total_entries >= $page_increment) { + $rows[] = array(array('data' => theme('pager', NULL, $page_increment), 'colspan' => '2')); + } + + return theme('table', $header, $rows, array('id' => 'taxonomy')); +} + +/** + * Display form for adding and editing vocabularies. + */ +function taxonomy_form_vocabulary($edit = array()) { + $form['name'] = array('#type' => 'textfield', + '#title' => t('Vocabulary name'), + '#default_value' => $edit['name'], + '#maxlength' => 64, + '#description' => t('The name for this vocabulary. Example: "Topic".'), + '#required' => TRUE, + ); + $form['description'] = array('#type' => 'textarea', + '#title' => t('Description'), + '#default_value' => $edit['description'], + '#description' => t('Description of the vocabulary; can be used by modules.'), + ); + $form['help'] = array('#type' => 'textfield', + '#title' => t('Help text'), + '#default_value' => $edit['help'], + '#description' => t('Instructions to present to the user when choosing a term.'), + ); + $form['nodes'] = array('#type' => 'checkboxes', + '#title' => t('Types'), + '#default_value' => $edit['nodes'], + '#options' => node_get_types(), + '#description' => t('A list of node types you want to associate with this vocabulary.'), + '#required' => TRUE, + ); + $form['hierarchy'] = array('#type' => 'radios', + '#title' => t('Hierarchy'), + '#default_value' => $edit['hierarchy'], + '#options' => array(t('Disabled'), t('Single'), t('Multiple')), + '#description' => t('Allows a tree-like hierarchy between terms of this vocabulary.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'hierarchy'))), + ); + $form['relations'] = array('#type' => 'checkbox', + '#title' => t('Related terms'), + '#default_value' => $edit['relations'], + '#description' => t('Allows related terms in this vocabulary.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'related-terms'))), + ); + $form['tags'] = array('#type' => 'checkbox', + '#title' => t('Free tagging'), + '#default_value' => $edit['tags'], + '#description' => t('Content is categorized by typing terms instead of choosing from a list.'), + ); + $form['multiple'] = array('#type' => 'checkbox', + '#title' => t('Multiple select'), + '#default_value' => $edit['multiple'], + '#description' => t('Allows nodes to have more than one term from this vocabulary (always true for free tagging).'), + ); + $form['required'] = array('#type' => 'checkbox', + '#title' => t('Required'), + '#default_value' => $edit['required'], + '#description' => t('If enabled, every node must have at least one term in this vocabulary.'), + ); + $form['weight'] = array('#type' => 'weight', + '#title' => t('Weight'), + '#default_value' => $edit['weight'], + '#description' => t('In listings, the heavier vocabularies will sink and the lighter vocabularies will be positioned nearer the top.'), + ); + + // Add extra vocabulary form elements. + $extra = module_invoke_all('taxonomy', 'form', 'vocabulary'); + if (is_array($extra)) { + foreach ($extra as $key => $element) { + $extra[$key]['#weight'] = isset($extra[$key]['#weight']) ? $extra[$key]['#weight'] : -18; + } + $form = array_merge($form, $extra); + } + + $form['submit'] = array('#type' => 'submit', '#value' => t('Submit')); + if ($edit['vid']) { + $form['delete'] = array('#type' => 'submit', '#value' => t('Delete')); + $form['vid'] = array('#type' => 'value', '#value' => $edit['vid']); + $form['module'] = array('#type' => 'value', '#value' => $edit['module']); + } + return drupal_get_form('taxonomy_form_vocabulary', $form); +} + +/** + * Accept the form submission for a vocabulary and save the results. + */ +function taxonomy_form_vocabulary_submit($form_id, $form_values) { + // Fix up the nodes array to remove unchecked nodes. + $form_values['nodes'] = array_filter($form_values['nodes']); + switch (taxonomy_save_vocabulary($form_values)) { + case SAVED_NEW: + drupal_set_message(t('Created new vocabulary %name.', array('%name' => theme('placeholder', $form_values['name'])))); + break; + case SAVED_UPDATED: + drupal_set_message(t('Updated vocabulary %name.', array('%name' => theme('placeholder', $form_values['name'])))); + break; + } + return 'admin/content/taxonomy'; +} + +function taxonomy_save_vocabulary(&$edit) { + $edit['nodes'] = empty($edit['nodes']) ? array() : $edit['nodes']; + + if ($edit['vid'] && $edit['name']) { + db_query("UPDATE {vocabulary} SET name = '%s', description = '%s', help = '%s', multiple = %d, required = %d, hierarchy = %d, relations = %d, tags = %d, weight = %d, module = '%s' WHERE vid = %d", $edit['name'], $edit['description'], $edit['help'], $edit['multiple'], $edit['required'], $edit['hierarchy'], $edit['relations'], $edit['tags'], $edit['weight'], isset($edit['module']) ? $edit['module'] : 'taxonomy', $edit['vid']); + db_query("DELETE FROM {vocabulary_node_types} WHERE vid = %d", $edit['vid']); + foreach ($edit['nodes'] as $type => $selected) { + db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type); + } + module_invoke_all('taxonomy', 'update', 'vocabulary', $edit); + $status = SAVED_UPDATED; + } + else if ($edit['vid']) { + $status = taxonomy_del_vocabulary($edit['vid']); + } + else { + $edit['vid'] = db_next_id('{vocabulary}_vid'); + db_query("INSERT INTO {vocabulary} (vid, name, description, help, multiple, required, hierarchy, relations, tags, weight, module) VALUES (%d, '%s', '%s', '%s', %d, %d, %d, %d, %d, %d, '%s')", $edit['vid'], $edit['name'], $edit['description'], $edit['help'], $edit['multiple'], $edit['required'], $edit['hierarchy'], $edit['relations'], $edit['tags'], $edit['weight'], isset($edit['module']) ? $edit['module'] : 'taxonomy'); + foreach ($edit['nodes'] as $type => $selected) { + db_query("INSERT INTO {vocabulary_node_types} (vid, type) VALUES (%d, '%s')", $edit['vid'], $type); + } + module_invoke_all('taxonomy', 'insert', 'vocabulary', $edit); + $status = SAVED_NEW; + } + + cache_clear_all(); + + return $status; +} + +function taxonomy_del_vocabulary($vid) { + $vocabulary = (array) taxonomy_get_vocabulary($vid); + + db_query('DELETE FROM {vocabulary} WHERE vid = %d', $vid); + db_query('DELETE FROM {vocabulary_node_types} WHERE vid = %d', $vid); + $result = db_query('SELECT tid FROM {term_data} WHERE vid = %d', $vid); + while ($term = db_fetch_object($result)) { + taxonomy_del_term($term->tid); + } + + module_invoke_all('taxonomy', 'delete', 'vocabulary', $vocabulary); + + cache_clear_all(); + + return SAVED_DELETED; +} + +function _taxonomy_confirm_del_vocabulary($vid) { + $vocabulary = taxonomy_get_vocabulary($vid); + + $form['type'] = array('#type' => 'value', '#value' => 'vocabulary'); + $form['vid'] = array('#type' => 'value', '#value' => $vid); + $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name); + return confirm_form('taxonomy_vocabulary_confirm_delete', $form, + t('Are you sure you want to delete the vocabulary %title?', + array('%title' => theme('placeholder', $vocabulary->name))), + 'admin/content/taxonomy', t('Deleting a vocabulary will delete all the terms in it. This action cannot be undone.'), + t('Delete'), + t('Cancel')); +} + +function taxonomy_vocabulary_confirm_delete_submit($form_id, $form_values) { + $status = taxonomy_del_vocabulary($form_values['vid']); + drupal_set_message(t('Deleted vocabulary %name.', array('%name' => theme('placeholder', $form_values['name'])))); + return 'admin/content/taxonomy'; +} + +function taxonomy_form_term($edit = array()) { + $vocabulary_id = isset($edit['vid']) ? $edit['vid'] : arg(4); + $vocabulary = taxonomy_get_vocabulary($vocabulary_id); + + $form['name'] = array('#type' => 'textfield', '#title' => t('Term name'), '#default_value' => $edit['name'], '#maxlength' => 64, '#description' => t('The name for this term. Example: "Linux".'), '#required' => TRUE); + + $form['description'] = array('#type' => 'textarea', '#title' => t('Description'), '#default_value' => $edit['description'], '#description' => t('A description of the term.')); + + if ($vocabulary->hierarchy) { + $parent = array_keys(taxonomy_get_parents($edit['tid'])); + $children = taxonomy_get_tree($vocabulary_id, $edit['tid']); + + // A term can't be the child of itself, nor of its children. + foreach ($children as $child) { + $exclude[] = $child->tid; + } + $exclude[] = $edit['tid']; + + if ($vocabulary->hierarchy == 1) { + $form['parent'] = _taxonomy_term_select(t('Parent'), 'parent', $parent, $vocabulary_id, l(t('Parent term'), 'admin/help/taxonomy', NULL, NULL, 'parent') .'.', 0, '<'. t('root') .'>', $exclude); + } + elseif ($vocabulary->hierarchy == 2) { + $form['parent'] = _taxonomy_term_select(t('Parents'), 'parent', $parent, $vocabulary_id, l(t('Parent terms'), 'admin/help/taxonomy', NULL, NULL, 'parent') .'.', 1, '<'. t('root') .'>', $exclude); + } + } + + if ($vocabulary->relations) { + $form['relations'] = _taxonomy_term_select(t('Related terms'), 'relations', array_keys(taxonomy_get_related($edit['tid'])), $vocabulary_id, NULL, 1, '<'. t('none') .'>', array($edit['tid'])); + } + + $form['synonyms'] = array('#type' => 'textarea', '#title' => t('Synonyms'), '#default_value' => implode("\n", taxonomy_get_synonyms($edit['tid'])), '#description' => t('Synonyms of this term, one synonym per line.', array('%help-url' => url('admin/help/taxonomy', NULL, NULL, 'synonyms')))); + $form['weight'] = array('#type' => 'weight', '#title' => t('Weight'), '#default_value' => $edit['weight'], '#description' => t('In listings, the heavier terms will sink and the lighter terms will be positioned nearer the top.')); + + // Add extra term form elements. + $extra = module_invoke_all('taxonomy', 'term', 'vocabulary'); + if (is_array($extra)) { + foreach ($extra as $key => $element) { + $extra[$key]['#weight'] = isset($extra[$key]['#weight']) ? $extra[$key]['#weight'] : -18; + } + $form = array_merge($form, $extra); + } + + + $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid); + $form['submit'] = array('#type' => 'submit', '#value' => t('Submit')); + + if ($edit['tid']) { + $form['delete'] = array('#type' => 'submit', '#value' => t('Delete')); + $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']); + } + else { + $form['destination'] = array('#type' => 'hidden', '#value' => $_GET['q']); + } + + return drupal_get_form('taxonomy_form_term', $form); +} + +/** + * Accept the form submission for a taxonomy term and save the result. + */ +function taxonomy_form_term_submit($form_id, $form_values) { + switch (taxonomy_save_term($form_values)) { + case SAVED_NEW: + drupal_set_message(t('Created new term %term.', array('%term' => theme('placeholder', $form_values['name'])))); + break; + case SAVED_UPDATED: + drupal_set_message(t('The term %term has been updated.', array('%term' => theme('placeholder', $form_values['name'])))); + break; + } + return 'admin/content/taxonomy'; +} + +function taxonomy_save_term(&$edit) { + if ($edit['tid'] && $edit['name']) { + db_query("UPDATE {term_data} SET name = '%s', description = '%s', weight = %d WHERE tid = %d", $edit['name'], $edit['description'], $edit['weight'], $edit['tid']); + module_invoke_all('taxonomy', 'update', 'term', $edit); + $status = SAVED_UPDATED; + } + else if ($edit['tid']) { + return taxonomy_del_term($edit['tid']); + } + else { + $edit['tid'] = db_next_id('{term_data}_tid'); + db_query("INSERT INTO {term_data} (tid, name, description, vid, weight) VALUES (%d, '%s', '%s', %d, %d)", $edit['tid'], $edit['name'], $edit['description'], $edit['vid'], $edit['weight']); + module_invoke_all('taxonomy', 'insert', 'term', $edit); + $status = SAVED_NEW; + } + + db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $edit['tid'], $edit['tid']); + if ($edit['relations']) { + foreach ($edit['relations'] as $related_id) { + if ($related_id != 0) { + db_query('INSERT INTO {term_relation} (tid1, tid2) VALUES (%d, %d)', $edit['tid'], $related_id); + } + } + } + + db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $edit['tid']); + if (!isset($edit['parent']) || empty($edit['parent'])) { + $edit['parent'] = array(0); + } + if (is_array($edit['parent'])) { + foreach ($edit['parent'] as $parent) { + if (is_array($parent)) { + foreach ($parent as $tid) { + db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $tid); + } + } + else { + db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $parent); + } + } + } + else { + db_query('INSERT INTO {term_hierarchy} (tid, parent) VALUES (%d, %d)', $edit['tid'], $edit['parent']); + } + + db_query('DELETE FROM {term_synonym} WHERE tid = %d', $edit['tid']); + if ($edit['synonyms']) { + foreach (explode ("\n", str_replace("\r", '', $edit['synonyms'])) as $synonym) { + if ($synonym) { + db_query("INSERT INTO {term_synonym} (tid, name) VALUES (%d, '%s')", $edit['tid'], chop($synonym)); + } + } + } + + cache_clear_all(); + + return $status; +} + +function taxonomy_del_term($tid) { + $tids = array($tid); + while ($tids) { + $children_tids = $orphans = array(); + foreach ($tids as $tid) { + // See if any of the term's children are about to be become orphans: + if ($children = taxonomy_get_children($tid)) { + foreach ($children as $child) { + // If the term has multiple parents, we don't delete it. + $parents = taxonomy_get_parents($child->tid); + if (count($parents) == 1) { + $orphans[] = $child->tid; + } + } + } + + $term = (array) taxonomy_get_term($tid); + + db_query('DELETE FROM {term_data} WHERE tid = %d', $tid); + db_query('DELETE FROM {term_hierarchy} WHERE tid = %d', $tid); + db_query('DELETE FROM {term_relation} WHERE tid1 = %d OR tid2 = %d', $tid, $tid); + db_query('DELETE FROM {term_synonym} WHERE tid = %d', $tid); + db_query('DELETE FROM {term_node} WHERE tid = %d', $tid); + + module_invoke_all('taxonomy', 'delete', 'term', $term); + } + + $tids = $orphans; + } + + cache_clear_all(); + + return SAVED_DELETED; +} + +function _taxonomy_confirm_del_term($tid) { + $term = taxonomy_get_term($tid); + + $form['type'] = array('#type' => 'value', '#value' => 'term'); + $form['name'] = array('#type' => 'value', '#value' => $term->name); + $form['tid'] = array('#type' => 'value', '#value' => $tid); + return confirm_form('taxonomy_term_confirm_delete', $form, + t('Are you sure you want to delete the term %title?', + array('%title' => theme('placeholder', $term->name))), + 'admin/content/taxonomy', + t('Deleting a term will delete all its children if there are any. This action cannot be undone.'), + t('Delete'), + t('Cancel')); +} + +function taxonomy_term_confirm_delete_submit($form_id, $form_values) { + taxonomy_del_term($form_values['tid']); + drupal_set_message(t('Deleted term %name.', array('%name' => theme('placeholder', $form_values['name'])))); + return 'admin/content/taxonomy'; +} + +/** + * Generate a form element for selecting terms from a vocabulary. + */ +function taxonomy_form($vid, $value = 0, $help = NULL, $name = 'taxonomy') { + $vocabulary = taxonomy_get_vocabulary($vid); + $help = ($help) ? $help : $vocabulary->help; + if ($vocabulary->required) { + $blank = 0; + } + else { + $blank = '<'. t('none') .'>'; + } + + return _taxonomy_term_select(check_plain($vocabulary->name), $name, $value, $vid, $help, intval($vocabulary->multiple), $blank); +} + +/** + * Generate a set of options for selecting a term from all vocabularies. Can be + * passed to form_select. + */ +function taxonomy_form_all($free_tags = 0) { + $vocabularies = taxonomy_get_vocabularies(); + $options = array(); + foreach ($vocabularies as $vid => $vocabulary) { + if ($vocabulary->tags && !$free_tags) { continue; } + $tree = taxonomy_get_tree($vid); + $options[$vocabulary->name] = array(); + if ($tree) { + foreach ($tree as $term) { + $options[$vocabulary->name][$term->tid] = _taxonomy_depth($term->depth, '-') . $term->name; + } + } + } + return $options; +} + +/** + * Return an array of all vocabulary objects. + * + * @param $type + * If set, return only those vocabularies associated with this node type. + */ +function taxonomy_get_vocabularies($type = NULL) { + if ($type) { + $result = db_query(db_rewrite_sql("SELECT v.vid, v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' ORDER BY v.weight, v.name", 'v', 'vid'), $type); + } + else { + $result = db_query(db_rewrite_sql('SELECT v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid ORDER BY v.weight, v.name', 'v', 'vid')); + } + + $vocabularies = array(); + $node_types = array(); + while ($voc = db_fetch_object($result)) { + $node_types[$voc->vid][] = $voc->type; + unset($voc->type); + $voc->nodes = $node_types[$voc->vid]; + $vocabularies[$voc->vid] = $voc; + } + + return $vocabularies; +} + +/** + * Generate a form for selecting terms to associate with a node. + */ +function taxonomy_form_alter($form_id, &$form) { + if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) { + $node = $form['#node']; + + if (!isset($node->taxonomy)) { + if ($node->nid) { + $terms = taxonomy_node_get_terms($node->nid); + } + else { + $terms = array(); + } + } + else { + $terms = $node->taxonomy; + } + + $c = db_query(db_rewrite_sql("SELECT v.* FROM {vocabulary} v INNER JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE n.type = '%s' ORDER BY v.weight, v.name", 'v', 'vid'), $node->type); + + while ($vocabulary = db_fetch_object($c)) { + if ($vocabulary->tags) { + $typed_terms = array(); + foreach ($terms as $term) { + // Extract terms belonging to the vocabulary in question. + if ($term->vid == $vocabulary->vid) { + + // Commas and quotes in terms are special cases, so encode 'em. + if (preg_match('/,/', $term->name) || preg_match('/"/', $term->name)) { + $term->name = '"'.preg_replace('/"/', '""', $term->name).'"'; + } + + $typed_terms[] = $term->name; + } + } + $typed_string = implode(', ', $typed_terms) . (array_key_exists('tags', $terms) ? $terms['tags'][$vocabulary->vid] : NULL); + + if ($vocabulary->help) { + $help = $vocabulary->help; + } + else { + $help = t('A comma-separated list of terms describing this content. Example: funny, bungee jumping, "Company, Inc.".'); + } + $form['taxonomy']['tags'][$vocabulary->vid] = array('#type' => 'textfield', + '#title' => $vocabulary->name, + '#description' => $help, + '#required' => $vocabulary->required, + '#default_value' => $typed_string, + '#autocomplete_path' => 'taxonomy/autocomplete/'. $vocabulary->vid, + '#weight' => $vocabulary->weight, + '#maxlength' => 255, + ); + } + else { + // Extract terms belonging to the vocabulary in question. + $default_terms = array(); + foreach ($terms as $term) { + if ($term->vid == $vocabulary->vid) { + $default_terms[$term->tid] = $term; + } + } + $form['taxonomy'][$vocabulary->vid] = taxonomy_form($vocabulary->vid, array_keys($default_terms), $vocabulary->help); + $form['taxonomy'][$vocabulary->vid]['#weight'] = $vocabulary->weight; + $form['taxonomy'][$vocabulary->vid]['#required'] = $vocabulary->required; + } + } + if (isset($form['taxonomy'])) { + $form['taxonomy'] += array('#type' => 'fieldset', '#title' => t('Categories'), '#collapsible' => TRUE, '#collapsed' => FALSE, '#tree' => TRUE, '#weight' => -3); + } + } +} + +/** + * Find all terms associated to the given node, within one vocabulary. + */ +function taxonomy_node_get_terms_by_vocabulary($nid, $vid, $key = 'tid') { + $result = db_query(db_rewrite_sql('SELECT t.tid, t.* FROM {term_data} t INNER JOIN {term_node} r ON r.tid = t.tid WHERE t.vid = %d AND r.nid = %d ORDER BY weight', 't', 'tid'), $vid, $nid); + $terms = array(); + while ($term = db_fetch_object($result)) { + $terms[$term->$key] = $term; + } + return $terms; +} + +/** + * Find all terms associated to the given node, ordered by vocabulary and term weight. + */ +function taxonomy_node_get_terms($nid, $key = 'tid') { + static $terms; + + if (!isset($terms[$nid])) { + $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.nid = %d ORDER BY v.weight, t.weight, t.name', 't', 'tid'), $nid); + $terms[$nid] = array(); + while ($term = db_fetch_object($result)) { + $terms[$nid][$term->$key] = $term; + } + } + return $terms[$nid]; +} + +/** + * Make sure incoming vids are free tagging enabled. + */ +function taxonomy_node_validate(&$node) { + if ($node->taxonomy) { + $terms = $node->taxonomy; + if ($terms['tags']) { + foreach ($terms['tags'] as $vid => $vid_value) { + $vocabulary = taxonomy_get_vocabulary($vid); + if (!$vocabulary->tags) { + // see form_get_error $key = implode('][', $element['#parents']); + // on why this is the key + form_set_error("taxonomy][tags][$vid", t('The %name vocabulary can not be modified in this way.', array('%name' => theme('placeholder', $vocabulary->name)))); + } + } + } + } +} + +/** + * Save term associations for a given node. + */ +function taxonomy_node_save($nid, $terms) { + taxonomy_node_delete($nid); + + // Free tagging vocabularies do not send their tids in the form, + // so we'll detect them here and process them independently. + if (isset($terms['tags'])) { + $typed_input = $terms['tags']; + unset($terms['tags']); + + foreach ($typed_input as $vid => $vid_value) { + // This regexp allows the following types of user input: + // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar + $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; + preg_match_all($regexp, $vid_value, $matches); + $typed_terms = array_unique($matches[1]); + + $inserted = array(); + foreach ($typed_terms as $typed_term) { + // If a user has escaped a term (to demonstrate that it is a group, + // or includes a comma or quote character), we remove the escape + // formatting so to save the term into the DB as the user intends. + $typed_term = str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $typed_term)); + $typed_term = trim($typed_term); + if ($typed_term == "") { continue; } + + // See if the term exists in the chosen vocabulary + // and return the tid, otherwise, add a new record. + $possibilities = taxonomy_get_term_by_name($typed_term); + $typed_term_tid = NULL; // tid match if any. + foreach ($possibilities as $possibility) { + if ($possibility->vid == $vid) { + $typed_term_tid = $possibility->tid; + } + } + + if (!$typed_term_tid) { + $edit = array('vid' => $vid, 'name' => $typed_term); + $status = taxonomy_save_term($edit); + $typed_term_tid = $edit['tid']; + } + + // Defend against duplicate, different cased tags + if (!isset($inserted[$typed_term_tid])) { + db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $typed_term_tid); + $inserted[$typed_term_tid] = TRUE; + } + } + } + } + + if (is_array($terms)) { + foreach ($terms as $term) { + if (is_array($term)) { + foreach ($term as $tid) { + if ($tid) { + db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $tid); + } + } + } + else if (is_object($term)) { + db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term->tid); + } + else if ($term) { + db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $nid, $term); + } + } + } +} + +/** + * Remove associations of a node to its terms. + */ +function taxonomy_node_delete($nid) { + db_query('DELETE FROM {term_node} WHERE nid = %d', $nid); +} + +/** + * Find all term objects related to a given term ID. + */ +function taxonomy_get_related($tid, $key = 'tid') { + if ($tid) { + $result = db_query('SELECT t.*, tid1, tid2 FROM {term_relation}, {term_data} t WHERE (t.tid = tid1 OR t.tid = tid2) AND (tid1 = %d OR tid2 = %d) AND t.tid != %d ORDER BY weight, name', $tid, $tid, $tid); + $related = array(); + while ($term = db_fetch_object($result)) { + $related[$term->$key] = $term; + } + return $related; + } + else { + return array(); + } +} + +/** + * Find all parents of a given term ID. + */ +function taxonomy_get_parents($tid, $key = 'tid') { + if ($tid) { + $result = db_query(db_rewrite_sql('SELECT t.tid, t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.parent = t.tid WHERE h.tid = %d ORDER BY weight, name', 't', 'tid'), $tid); + $parents = array(); + while ($parent = db_fetch_object($result)) { + $parents[$parent->$key] = $parent; + } + return $parents; + } + else { + return array(); + } +} + +/** + * Find all ancestors of a given term ID. + */ +function taxonomy_get_parents_all($tid) { + $parents = array(); + if ($tid) { + $parents[] = taxonomy_get_term($tid); + $n = 0; + while ($parent = taxonomy_get_parents($parents[$n]->tid)) { + $parents = array_merge($parents, $parent); + $n++; + } + } + return $parents; +} + +/** + * Find all children of a term ID. + */ +function taxonomy_get_children($tid, $vid = 0, $key = 'tid') { + if ($vid) { + $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.tid = t.tid WHERE t.vid = %d AND h.parent = %d ORDER BY weight, name', 't', 'tid'), $vid, $tid); + } + else { + $result = db_query(db_rewrite_sql('SELECT t.* FROM {term_data} t INNER JOIN {term_hierarchy} h ON h.tid = t.tid WHERE parent = %d ORDER BY weight, name', 't', 'tid'), $tid); + } + $children = array(); + while ($term = db_fetch_object($result)) { + $children[$term->$key] = $term; + } + return $children; +} + +/** + * Create a hierarchical representation of a vocabulary. + * + * @param $vid + * Which vocabulary to generate the tree for. + * + * @param $parent + * The term ID under which to generate the tree. If 0, generate the tree + * for the entire vocabulary. + * + * @param $depth + * Internal use only. + * + * @param $max_depth + * The number of levels of the tree to return. Leave NULL to return all levels. + * + * @return + * An array of all term objects in the tree. Each term object is extended + * to have "depth" and "parents" attributes in addition to its normal ones. + */ +function taxonomy_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL) { + static $children, $parents, $terms; + + $depth++; + + // We cache trees, so it's not CPU-intensive to call get_tree() on a term + // and its children, too. + if (!isset($children[$vid])) { + $children[$vid] = array(); + + $result = db_query(db_rewrite_sql('SELECT t.tid, t.*, parent FROM {term_data} t INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE t.vid = %d ORDER BY weight, name', 't', 'tid'), $vid); + while ($term = db_fetch_object($result)) { + $children[$vid][$term->parent][] = $term->tid; + $parents[$vid][$term->tid][] = $term->parent; + $terms[$vid][$term->tid] = $term; + } + } + + $max_depth = (is_null($max_depth)) ? count($children[$vid]) : $max_depth; + if ($children[$vid][$parent]) { + foreach ($children[$vid][$parent] as $child) { + if ($max_depth > $depth) { + $terms[$vid][$child]->depth = $depth; + // The "parent" attribute is not useful, as it would show one parent only. + unset($terms[$vid][$child]->parent); + $terms[$vid][$child]->parents = $parents[$vid][$child]; + $tree[] = $terms[$vid][$child]; + + if ($children[$vid][$child]) { + $tree = array_merge($tree, taxonomy_get_tree($vid, $child, $depth, $max_depth)); + } + } + } + } + + return $tree ? $tree : array(); +} + +/** + * Return an array of synonyms of the given term ID. + */ +function taxonomy_get_synonyms($tid) { + if ($tid) { + $result = db_query('SELECT name FROM {term_synonym} WHERE tid = %d', $tid); + while ($synonym = db_fetch_array($result)) { + $synonyms[] = $synonym['name']; + } + return $synonyms ? $synonyms : array(); + } + else { + return array(); + } +} + +/** + * Return the term object that has the given string as a synonym. + */ +function taxonomy_get_synonym_root($synonym) { + return db_fetch_object(db_query("SELECT * FROM {term_synonym} s, {term_data} t WHERE t.tid = s.tid AND s.name = '%s'", $synonym)); +} + +/** + * Given a term id, count the number of published nodes in it. + */ +function taxonomy_term_count_nodes($tid, $type = 0) { + static $count; + + if (!isset($count[$type])) { + // $type == 0 always evaluates TRUE is $type is a string + if (is_numeric($type)) { + $result = db_query(db_rewrite_sql('SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE n.status = 1 GROUP BY t.tid')); + } + else { + $result = db_query(db_rewrite_sql("SELECT t.tid, COUNT(n.nid) AS c FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE n.status = 1 AND n.type = '%s' GROUP BY t.tid"), $type); + } + while ($term = db_fetch_object($result)) { + $count[$type][$term->tid] = $term->c; + } + } + + foreach (_taxonomy_term_children($tid) as $c) { + $children_count += taxonomy_term_count_nodes($c, $type); + } + return $count[$type][$tid] + $children_count; +} + +/** + * Helper for taxonomy_term_count_nodes(). + */ +function _taxonomy_term_children($tid) { + static $children; + + if (!isset($children)) { + $result = db_query('SELECT tid, parent FROM {term_hierarchy}'); + while ($term = db_fetch_object($result)) { + $children[$term->parent][] = $term->tid; + } + } + return $children[$tid] ? $children[$tid] : array(); +} + +/** + * Try to map a string to an existing term, as for glossary use. + * + * Provides a case-insensitive and trimmed mapping, to maximize the + * likelihood of a successful match. + * + * @param name + * Name of the term to search for. + * + * @return + * An array of matching term objects. + */ +function taxonomy_get_term_by_name($name) { + $db_result = db_query(db_rewrite_sql("SELECT t.tid, t.* FROM {term_data} t WHERE LOWER('%s') LIKE LOWER(t.name)", 't', 'tid'), trim($name)); + $result = array(); + while ($term = db_fetch_object($db_result)) { + $result[] = $term; + } + + return $result; +} + +/** + * Return the vocabulary object matching a vocabulary ID. + */ +function taxonomy_get_vocabulary($vid) { + static $vocabularies = array(); + + if (!array_key_exists($vid, $vocabularies)) { + $result = db_query('SELECT v.*, n.type FROM {vocabulary} v LEFT JOIN {vocabulary_node_types} n ON v.vid = n.vid WHERE v.vid = %d ORDER BY v.weight, v.name', $vid); + $node_types = array(); + while ($voc = db_fetch_object($result)) { + $node_types[] = $voc->type; + unset($voc->type); + $voc->nodes = $node_types; + $vocabularies[$vid] = $voc; + } + } + + return $vocabularies[$vid]; +} + +/** + * Return the term object matching a term ID. + */ +function taxonomy_get_term($tid) { + // simple cache using a static var? + return db_fetch_object(db_query('SELECT * FROM {term_data} WHERE tid = %d', $tid)); +} + +function _taxonomy_term_select($title, $name, $value, $vocabulary_id, $description, $multiple, $blank, $exclude = array()) { + $tree = taxonomy_get_tree($vocabulary_id); + $options = array(); + + if ($blank) { + $options[0] = $blank; + } + if ($tree) { + foreach ($tree as $term) { + if (!in_array($term->tid, $exclude)) { + $options[$term->tid] = _taxonomy_depth($term->depth, '-') . $term->name; + } + } + if (!$blank && !$value) { + // required but without a predefined value, so set first as predefined + $value = $tree[0]->tid; + } + } + + return array('#type' => 'select', + '#title' => $title, + '#default_value' => $value, + '#options' => $options, + '#description' => $description, + '#multiple' => $multiple, + '#size' => $multiple ? min(9, count($options)) : 0, + '#weight' => -15, + '#theme' => 'taxonomy_term_select', + ); +} + +function theme_taxonomy_term_select($element) { + return theme('select', $element); +} + +function _taxonomy_depth($depth, $graphic = '--') { + for ($n = 0; $n < $depth; $n++) { + $result .= $graphic; + } + return $result; +} + +/** + * Finds all nodes that match selected taxonomy conditions. + * + * @param $tids + * An array of term IDs to match. + * @param $operator + * How to interpret multiple IDs in the array. Can be "or" or "and". + * @param $depth + * How many levels deep to traverse the taxonomy tree. Can be a nonnegative + * integer or "all". + * @param $pager + * Whether the nodes are to be used with a pager (the case on most Drupal + * pages) or not (in an XML feed, for example). + * @param $order + * The order clause for the query that retrieve the nodes. + * @return + * A resource identifier pointing to the query results. + */ +function taxonomy_select_nodes($tids = array(), $operator = 'or', $depth = 0, $pager = TRUE, $order = 'n.sticky DESC, n.created DESC') { + if (count($tids) > 0) { + // For each term ID, generate an array of descendant term IDs to the right depth. + $descendant_tids = array(); + if ($depth === 'all') { + $depth = NULL; + } + foreach ($tids as $index => $tid) { + $term = taxonomy_get_term($tid); + $tree = taxonomy_get_tree($term->vid, $tid, -1, $depth); + $descendant_tids[] = array_merge(array($tid), array_map('_taxonomy_get_tid_from_term', $tree)); + } + + if ($operator == 'or') { + $str_tids = implode(',', call_user_func_array('array_merge', $descendant_tids)); + $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1 ORDER BY '. $order; + $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid WHERE tn.tid IN ('. $str_tids .') AND n.status = 1'; + } + else { + $joins = ''; + $wheres = ''; + foreach ($descendant_tids as $index => $tids) { + $joins .= ' INNER JOIN {term_node} tn'. $index .' ON n.nid = tn'. $index .'.nid'; + $wheres .= ' AND tn'. $index .'.tid IN ('. implode(',', $tids) .')'; + } + $sql = 'SELECT DISTINCT(n.nid), n.sticky, n.title, n.created FROM {node} n '. $joins .' WHERE n.status = 1 '. $wheres .' ORDER BY '. $order; + $sql_count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n '. $joins .' WHERE n.status = 1 ' . $wheres; + } + $sql = db_rewrite_sql($sql); + $sql_count = db_rewrite_sql($sql_count); + if ($pager) { + $result = pager_query($sql, variable_get('default_nodes_main', 10), 0, $sql_count); + } + else { + $result = db_query_range($sql, 0, variable_get('feed_default_items', 10)); + } + } + + return $result; +} + +/** + * Accepts the result of a pager_query() call, such as that performed by + * taxonomy_select_nodes(), and formats each node along with a pager. +*/ +function taxonomy_render_nodes($result) { + if (db_num_rows($result) > 0) { + while ($node = db_fetch_object($result)) { + $output .= node_view(node_load($node->nid), 1); + } + $output .= theme('pager', NULL, variable_get('default_nodes_main', 10), 0); + } + else { + $output .= '

    '. t('There are currently no posts in this category.') .'

    '; + } + return $output; +} + +/** + * Implementation of hook_nodeapi(). + */ +function taxonomy_nodeapi($node, $op, $arg = 0) { + switch ($op) { + case 'load': + $output['taxonomy'] = taxonomy_node_get_terms($node->nid); + return $output; + case 'insert': + taxonomy_node_save($node->nid, $node->taxonomy); + break; + case 'update': + taxonomy_node_save($node->nid, $node->taxonomy); + break; + case 'delete': + taxonomy_node_delete($node->nid); + break; + case 'validate': + taxonomy_node_validate($node); + break; + case 'rss item': + return taxonomy_rss_item($node); + case 'update index': + return taxonomy_node_update_index($node); + } +} + +/** + * Implementation of hook_nodeapi('update_index'). + */ +function taxonomy_node_update_index(&$node) { + $output = array(); + foreach ($node->taxonomy as $term) { + $output[] = $term->name; + } + if (count($output)) { + return '('. implode(', ', $output) .')'; + } +} + +/** + * Parses a comma or plus separated string of term ids. + * + * @param $str_tids + * An string of term ids, separated by plus or comma. + * comma (,) means AND + * plus (+) means OR + * + * @return an associative array with an operator key (either 'and' + * or 'or') and an array, tids, containing the term ids. + */ +function taxonomy_terms_parse_string($str_tids) { + $terms = array(); + if (preg_match('/^([0-9]+[+ ])+[0-9]+$/', $str_tids)) { + $terms['operator'] = 'or'; + // The '+' character in a query string may be parsed as ' '. + $terms['tids'] = preg_split('/[+ ]/', $str_tids); + } + else if (preg_match('/^([0-9]+,)*[0-9]+$/', $str_tids)) { + $terms['operator'] = 'and'; + $terms['tids'] = explode(',', $str_tids); + } + return $terms; +} + + +/** + * Menu callback; displays all nodes associated with a term. + */ +function taxonomy_term_page($str_tids = '', $depth = 0, $op = 'page') { + $terms = taxonomy_terms_parse_string($str_tids); + if ($terms['operator'] != 'and' && $terms['operator'] != 'or') { + drupal_not_found(); + } + + if ($terms['tids']) { + $result = db_query(db_rewrite_sql('SELECT t.tid, t.name FROM {term_data} t WHERE t.tid IN (%s)', 't', 'tid'), implode(',', $terms['tids'])); + $tids = array(); // we rebuild the $tids-array so it only contains terms the user has access to. + $names = array(); + while ($term = db_fetch_object($result)) { + $tids[] = $term->tid; + $names[] = $term->name; + } + + if ($names) { + $title = check_plain(implode(', ', $names)); + drupal_set_title($title); + + switch ($op) { + case 'page': + // Build breadcrumb based on first hierarchy of first term: + $current->tid = $tids[0]; + $breadcrumbs = array(array('path' => $_GET['q'], 'title' => $names[0])); + while ($parents = taxonomy_get_parents($current->tid)) { + $current = array_shift($parents); + $breadcrumbs[] = array('path' => 'taxonomy/term/'. $current->tid, 'title' => $current->name); + } + $breadcrumbs = array_reverse($breadcrumbs); + menu_set_location($breadcrumbs); + + drupal_add_link(array('rel' => 'alternate', + 'type' => 'application/rss+xml', + 'title' => 'RSS - '. $title, + 'href' => url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed'))); + + $output = taxonomy_render_nodes(taxonomy_select_nodes($tids, $terms['operator'], $depth, TRUE)); + $output .= theme('feed_icon', url('taxonomy/term/'. $str_tids .'/'. $depth .'/feed')); + return $output; + break; + + case 'feed': + $term = taxonomy_get_term($tids[0]); + $channel['link'] = url('taxonomy/term/'. $str_tids .'/'. $depth, NULL, NULL, TRUE); + $channel['title'] = variable_get('site_name', 'drupal') .' - '. $title; + $channel['description'] = $term->description; + + $result = taxonomy_select_nodes($tids, $terms['operator'], $depth, FALSE); + node_feed($result, $channel); + break; + default: + drupal_not_found(); + } + } + else { + drupal_not_found(); + } + } +} + +/** + * Page to add or edit a vocabulary + */ +function taxonomy_admin_vocabulary_edit($vid = NULL) { + if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) { + return _taxonomy_confirm_del_vocabulary($vid); + } + elseif ($vid) { + $vocabulary = (array)taxonomy_get_vocabulary($vid); + } + return taxonomy_form_vocabulary($vocabulary); +} + +/** + * Page to list terms for a vocabulary + */ +function taxonomy_admin_term_edit($tid = NULL) { + if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) { + return _taxonomy_confirm_del_term($tid); + } + elseif ($tid) { + $term = (array)taxonomy_get_term($tid); + } + return taxonomy_form_term($term); +} + +/** + * Provides category information for rss feeds + */ +function taxonomy_rss_item($node) { + $output = array(); + foreach ($node->taxonomy as $term) { + $output[] = array('key' => 'category', + 'value' => check_plain($term->name), + 'attributes' => array('domain' => url('taxonomy/term/'. $term->tid, NULL, NULL, TRUE))); + } + return $output; +} + +/** + * Implementation of hook_help(). + */ +function taxonomy_help($section) { + switch ($section) { + case 'admin/help#taxonomy': + $output = '

    '. t('The taxonomy module is one of the most popular features because users often want to create categories to organize content by type. It can automatically classify new content, which is very useful for organizing content on-the-fly. A simple example would be organizing a list of music reviews by musical genre.') .'

    '; + $output .= '

    '. t('Taxonomy is also the study of classification. The taxonomy module allows you to define vocabularies (sets of categories) which are used to classify content. The module supports hierarchical classification and association between terms, allowing for truly flexible information retrieval and classification. The taxonomy module allows multiple lists of categories for classification (controlled vocabularies) and offers the possibility of creating thesauri (controlled vocabularies that indicate the relationship of terms) and taxonomies (controlled vocabularies where relationships are indicated hierarchically). To view and manage the terms of each vocabulary, click on the associated list terms link. To delete a vocabulary and all its terms, choose edit vocabulary.') .'

    '; + $output .= '

    '. t('A controlled vocabulary is a set of terms to use for describing content (known as descriptors in indexing lingo). Drupal allows you to describe each piece of content (blog, story, etc.) using one or many of these terms. For simple implementations, you might create a set of categories without subcategories, similar to Slashdot\'s sections. For more complex implementations, you might create a hierarchical list of categories.') .'

    '; + $output .= t('

    You can

    + +', array('%admin-taxonomy-add-vocabulary' => url('admin/content/taxonomy/add/vocabulary'), '%admin-taxonomy' => url('admin/content/taxonomy'), '%external-http-drupal-org-project-taxonomy_access' => 'http://drupal.org/project/taxonomy_access', '%external-http-drupal-org-project-taxonomy_browser' => 'http://drupal.org/project/taxonomy_browser')); + $output .= '

    '. t('For more information please read the configuration and customization handbook Taxonomy page.', array('%taxonomy' => 'http://drupal.org/handbook/modules/taxonomy/')) .'

    '; + return $output; + case 'admin/settings/modules#description': + return t('Enables the categorization of content.'); + case 'admin/content/taxonomy': + return t('

    The taxonomy module allows you to classify content into categories and subcategories; it allows multiple lists of categories for classification (controlled vocabularies) and offers the possibility of creating thesauri (controlled vocabularies that indicate the relationship of terms), taxonomies (controlled vocabularies where relationships are indicated hierarchically), and free vocabularies where terms, or tags, are defined during content creation. To view and manage the terms of each vocabulary, click on the associated list terms link. To delete a vocabulary and all its terms, choose "edit vocabulary".

    '); + case 'admin/content/taxonomy/add/vocabulary': + return t("

    When you create a controlled vocabulary you are creating a set of terms to use for describing content (known as descriptors in indexing lingo). Drupal allows you to describe each piece of content (blog, story, etc.) using one or many of these terms. For simple implementations, you might create a set of categories without subcategories, similar to Slashdot.org's or Kuro5hin.org's sections. For more complex implementations, you might create a hierarchical list of categories.

    "); + } +} + +/** + * Helper function for array_map purposes. + */ +function _taxonomy_get_tid_from_term($term) { + return $term->tid; +} + +/** + * Helper function for autocompletion + */ +function taxonomy_autocomplete($vid, $string = '') { + // The user enters a comma-separated list of tags. We only autocomplete the last tag. + // This regexp allows the following types of user input: + // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar + $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; + preg_match_all($regexp, $string, $matches); + $array = $matches[1]; + + // Fetch last tag + $last_string = trim(array_pop($array)); + if ($last_string != '') { + $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_string, 0, 10); + + $prefix = count($array) ? implode(', ', $array) .', ' : ''; + + $matches = array(); + while ($tag = db_fetch_object($result)) { + $n = $tag->name; + // Commas and quotes in terms are special cases, so encode 'em. + if (preg_match('/,/', $tag->name) || preg_match('/"/', $tag->name)) { + $n = '"'. preg_replace('/"/', '""', $tag->name) .'"'; + } + $matches[$prefix . $n] = check_plain($tag->name); + } + print drupal_to_js($matches); + exit(); + } +} diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/taxonomy/taxonomy.module ./modules/taxonomy/taxonomy.module --- ../C-HEAD/modules/taxonomy/taxonomy.module 2006-08-02 16:07:13.000000000 -0700 +++ ./modules/taxonomy/taxonomy.module 2006-08-02 15:50:00.000000000 -0700 @@ -138,7 +138,7 @@ function taxonomy_overview_vocabularies( foreach ($vocabularies as $vocabulary) { $types = array(); foreach ($vocabulary->nodes as $type) { - $node_type = node_get_name($type); + $node_type = node_get_types('name', $type); $types[] = $node_type ? $node_type : $type; } $rows[] = array('name' => check_plain($vocabulary->name), diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/tracker/tracker.module ./modules/tracker/tracker.module --- ../C-HEAD/modules/tracker/tracker.module 2006-07-31 13:14:30.000000000 -0700 +++ ./modules/tracker/tracker.module 2006-07-31 23:44:38.000000000 -0700 @@ -110,7 +110,7 @@ function tracker_page($uid = 0) { } $rows[] = array( - node_get_name($node->type), + node_get_types('name', $node->type), l($node->title, "node/$node->nid") .' '. theme('mark', node_mark($node->nid, $node->changed)), theme('username', $node), array('class' => 'replies', 'data' => $comments), diff -r'uNF^f' --exclude=CVS --exclude=docs --exclude=files --exclude sites ../C-HEAD/modules/upload/upload.module ./modules/upload/upload.module --- ../C-HEAD/modules/upload/upload.module 2006-07-31 13:14:30.000000000 -0700 +++ ./modules/upload/upload.module 2006-07-31 23:44:39.000000000 -0700 @@ -292,14 +292,16 @@ function _upload_prepare(&$node) { } function upload_form_alter($form_id, &$form) { - if (isset($form['type'])) { - if ($form['type']['#value'] .'_node_settings' == $form_id) { - $form['workflow']['upload_'. $form['type']['#value']] = array( - '#type' => 'radios', '#title' => t('Attachments'), '#default_value' => variable_get('upload_'. $form['type']['#value'], 1), - '#options' => array(t('Disabled'), t('Enabled')), - ); - } + if ($form_id == 'node_type_form' && isset($form['identity']['type'])) { + $form['workflow']['upload'] = array( + '#type' => 'radios', + '#title' => t('Attachments'), + '#default_value' => variable_get('upload_'. $form['identity']['type']['#default_value'], 1), + '#options' => array(t('Disabled'), t('Enabled')), + ); + } + if (isset($form['type'])) { $node = $form['#node']; if ($form['type']['#value'] .'_node_form' == $form_id && variable_get("upload_$node->type", TRUE) && user_access('upload files')) { drupal_add_js('misc/progress.js');