'admin/logs/journal', 'title' => t('Journal entries'), 'description' => t('View journal entries.'), 'callback' => 'journal_view', 'access' => $access, ); $items[] = array( 'path' => 'admin/logs/journal/list', 'title' => t('List'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); $items[] = array( 'path' => 'admin/logs/journal/patches', 'title' => t('Patches'), 'description' => t('View list of applied patches and hacks on this Drupal site.'), 'callback' => 'journal_patch_view', 'access' => $access, 'type' => MENU_LOCAL_TASK, ); $items[] = array( 'path' => 'admin/logs/journal/patches/edit', 'title' => t('Edit patch'), 'callback' => 'drupal_get_form', 'callback arguments' => array('journal_patch_form'), 'access' => $access, 'type' => MENU_CALLBACK, ); $items[] = array( 'path' => 'admin/logs/journal/patches/delete', 'callback' => 'drupal_get_form', 'callback arguments' => array('journal_patch_delete_confirm'), 'access' => $access, 'type' => MENU_CALLBACK, ); } return $items; } /** * Add Journal fields to all forms. * * Any form, except user-defined form_ids, will be extended by a fieldset * to enter a journal entry. All journal form ids are stored in one variable * array; having form_ids as keys and a boolean value whether to skip a form id * (0) or force/require a journal entry for it (1). * * @see journal_form_ids_default() */ function journal_form_alter($form_id, &$form) { if (!user_access('access journal')) { return; } $entry_required = FALSE; if (!isset($form['#submit'])) { $form['#submit'] = array(); } // Check whether form has to/must not be extended. $journal_ids = array_merge(journal_form_ids_default(), variable_get('journal_form_ids', array())); if (isset($journal_ids[$form_id]) || $form['#base'] == 'system_settings_form') { if (isset($journal_ids[$form_id]) && !$journal_ids[$form_id]) { // No journal entry for 'form_id' => 0. return; } else { // Require journal entry for 'form_id' => 1 or system settings forms. $entry_required = TRUE; } } // Shift system_settings_form buttons. if (isset($form['#base']) && $form['#base'] == 'system_settings_form') { $weight = isset($form['buttons']['#weight']) ? $form['buttons']['#weight'] : 0; $form['buttons']['#weight'] = $weight + 2; $journal_weight = $weight + 1; $entry_required = TRUE; } else { $journal_weight = 100; } // Prepend our journal submit handler, so we can eliminate the form value of // journal_entry, which would be saved as a variable in system_settings_form() // otherwise. $form['#submit'] = array('journal_form_submit' => array()) + $form['#submit']; $form['journal'] = array( '#weight' => $journal_weight, '#tree' => FALSE, ); // Store the path on which this form was initially displayed. // We need to store this in a hidden field, since forms with custom '#action's // (like admin/build/modules) will reset our value to $_GET['q'] otherwise. $form['journal']['journal_location'] = array( '#type' => 'hidden', '#value' => (!empty($_REQUEST['journal_location']) ? $_REQUEST['journal_location'] : $_GET['q']), ); // Add journal entry field. $form['journal']['journal_entry'] = array( '#type' => 'textarea', '#title' => t('Journal entry'), '#description' => t('If not empty, contents of this field will be logged to the system journal.'), '#required' => $entry_required, '#wysiwyg' => FALSE, ); if ($entry_required && user_access('access devel information')) { $form['journal']['journal_entry']['#required'] = FALSE; $form['journal']['journal_omit'] = array( '#type' => 'checkbox', '#title' => t('Skip journal entry'), '#return_value' => 1, '#default_value' => 0, '#description' => t('The journal entry for this form is required. If enabled, the form can be submitted without a journal entry.'), ); $form['#validate']['journal_form_validate'] = array(); } } /** * Validate optional journal entry for privileged users. */ function journal_form_validate($form_id, $form_values) { if (empty($form_values['journal_omit']) && empty($form_values['journal_entry'])) { form_set_error('journal', t('!name field is required.', array('!name' => t('Journal entry')))); } } /** * Save a new journal entry and clean out form values. */ function journal_form_submit($form_id, &$form_values) { if (!empty($form_values['journal_entry'])) { journal_add_entry($form_values['journal_entry'], $form_values['journal_location']); } unset($form_values['journal_entry'], $form_values['journal_location']); } /** * Indicate if a form must not extended. * * @param string $form_id * A form_id to check against. * * @return bool * True if form should be skipped, false if form can be extended. * * @todo Introduce a new FAPI attribute #journal = TRUE to require a journal * entry if Journal module is enabled - OR - introduce a new hook_journal? */ function journal_form_ids_default() { return array( 'devel_admin_settings' => 0, 'devel_execute_form' => 0, 'devel_switch_user_form' => 0, 'journal_patch_form' => 0, 'node_filter_form' => 0, 'poll_view_voting' => 0, 'search_block_form' => 0, 'search_box_form' => 0, 'search_form' => 0, 'search_theme_form' => 0, 'system_modules' => 1, 'user_admin_perm' => 1, 'user_filter_form' => 0, 'user_login_block' => 0, 'views_exposed_form' => 0, 'views_filters' => 0, 'views_ui_add_display_form' => 0, 'views_ui_add_item_form' => 0, 'views_ui_analyze_view_button' => 0, 'views_ui_config_item_form' => 0, 'views_ui_config_type_form' => 0, 'views_ui_edit_details_form' => 0, 'views_ui_edit_display_form' => 0, 'views_ui_export_page' => 0, 'views_ui_list_views_form' => 0, 'views_ui_preview_form' => 0, 'views_ui_rearrange_form' => 0, 'views_ui_remove_display_form' => 0, 'watchdog_form_overview' => 0, ); } /** * Implementation of hook_user(). */ function journal_user($op, &$edit, &$user) { if ($op == 'delete') { db_query('UPDATE {journal} SET uid = 0 WHERE uid = %d', $user->uid); } } /** * Implementation of hook_block(). */ function journal_block($op = 'list', $delta = 0, $edit = array()) { if ($op == 'list') { $blocks = array(); $blocks['backlog'] = array( 'info' => t('Journal entries'), 'weight' => -10, 'enabled' => 1, 'region' => 'right', ); return $blocks; } else if ($op == 'view' && user_access('access journal')) { $block = array(); switch ($delta) { case 'backlog': $result = db_query("SELECT j.*, u.name FROM {journal} j INNER JOIN {users} u ON j.uid = u.uid WHERE j.location = '%s' ORDER BY j.timestamp DESC", $_GET['q']); if ($output = journal_output($result, 'list')) { drupal_add_css(drupal_get_path('module', 'journal') .'/journal.css', 'module', 'all', FALSE); $block = array( 'subject' => t('Journal entries'), 'content' => $output, ); } break; } return $block; } } /** * Output a sortable table containing all journal entries. */ function journal_view() { $sql = "SELECT j.*, u.name FROM {journal} j INNER JOIN {users} u ON j.uid = u.uid"; $header = array( array('data' => t('Date'), 'field' => 'j.timestamp', 'sort' => 'desc'), array('data' => t('User'), 'field' => 'u.name'), t('Message'), t('Location'), ); $tablesort = tablesort_sql($header); $result = pager_query($sql . $tablesort, 50); return journal_output($result, 'table', $header); } /** * Render journal entries. * * Use this function to render and return * - a journal provided as a database query result resource or * - a custom journal provided as an array containing journal entry objects. * * This function may look insane to some, but it ensures that implementation of * journal module into other modules is as easy as possible. * * @param array $journal * A database query result resource or an array containing journal entry * objects to output. * @param string $format * Whether to output all log entries as 'table', 'list' or plain 'text'. * @param array $header * An array containing header data for 'table' output. * * @todo Add XML output format. */ function journal_output($journal, $format = 'table', $header = array()) { switch ($format) { case 'text': // Output delimiter in first line, since this may change. $output = '\t' . "\n"; while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) { $row = array( $entry->timestamp, $entry->uid, $entry->message, $entry->location, ); $output .= implode("\t", $row) ."\n"; } break; case 'list': $output = ''; while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) { $output .= '
  • '; $output .= ''. theme('username', $entry) .' '. format_date($entry->timestamp, 'small') .':'; $output .= ''. filter_xss_admin($entry->message) .''; $output .= '
  • '; } if ($output) { $output = ''; } break; case 'table': default: $rows = array(); while ($entry = (is_array($journal) ? array_shift($journal) : db_fetch_object($journal))) { $rows[] = array( format_date($entry->timestamp, 'small'), theme('username', $entry), filter_xss_admin($entry->message), l(truncate_utf8($entry->location, 32, FALSE, TRUE), $entry->location), ); } if (empty($rows)) { $rows[] = array(array('data' => t('No journal entries available.'), 'colspan' => 4)); } $output = theme('table', $header, $rows); $output .= theme('pager', NULL, 50, 0); break; } return $output; } /** * Convert a journal into an array. * * @param mixed $data * Journal data. * @param string $type * The type of passed-in journal data. * * @return array $journal * An array containing one or more journal entry objects. */ function journal_convert($data, $type = 'text') { $journal = array(); switch ($type) { case 'text': default: $data = explode('\n', $data); // Determine delimiter string. $delimiter = array_shift($data); while ($row = array_shift($data)) { $row = explode($delimiter, $row); $journal[] = (object)$row; } break; } return $journal; } /** * Store a new journal entry in the database. * * @param string $description * A journal entry text entered by an user. * @param string $location * The path on which the journal entry has been entered. */ function journal_add_entry($description, $location) { global $user; $jid = db_next_id("{journal}_jid"); db_query("INSERT INTO {journal} (jid, uid, message, location, timestamp) VALUES (%d, %d, '%s', '%s', %d)", $jid, $user->uid, $description, $location, time()); } /** * Output a sortable table containing all recorded patches. */ function journal_patch_view() { $header = array( array('data' => t('Date'), 'field' => 'j.timestamp', 'sort' => 'desc'), array('data' => t('Module'), 'field' => 'j.module'), array('data' => t('User'), 'field' => 'u.name'), t('Description'), t('Issue'), t('Status'), t('Operations'), ); $sql = "SELECT j.*, u.name FROM {journal_patch} j INNER JOIN {users} u ON j.uid = u.uid"; $result = pager_query($sql . tablesort_sql($header), 25); $module_list = module_list(FALSE, FALSE); $rows = array(); while ($entry = db_fetch_object($result)) { $modules = array(); foreach (explode(',', $entry->module) as $module) { $info = _module_parse_info_file(drupal_get_path('module', $module) .'/'. $module .'.info'); if (isset($info['project'])) { $url = 'http://drupal.org/project/issues/'. $info['project']; $modules[] = l($info['name'], $url); } else { $modules[] = $info['name']; } } if ($entry->url != '') { if (preg_match('@drupal.org/node/(\d+)@', $entry->url, $issue_title)) { $issue_link = l('#'. $issue_title[1], $entry->url); } else { $issue_link = l(t('View'), $entry->url); } } else { $issue_link = ''; } $rows[] = array( format_date($entry->timestamp, 'small'), implode(', ', $modules), theme('username', $entry), filter_xss_admin($entry->description), $issue_link, t($entry->status), l(t('edit'), "admin/logs/journal/patches/edit/$entry->pid"), ); } if (empty($rows)) { $rows[] = array(array('data' => t('No patch entries available.'), 'colspan' => 7)); } $output = drupal_get_form('journal_patch_form'); $output .= theme('table', $header, $rows); $output .= theme('pager', NULL, 50, 0); return $output; } /** * Form builder function for patches. */ function journal_patch_form($pid = NULL) { drupal_add_css(drupal_get_path('module', 'journal') .'/journal_patch.css', 'module', 'all', FALSE); $patch = array(); if (isset($pid)) { $patch = db_fetch_array(db_query("SELECT j.* FROM {journal_patch} j WHERE j.pid = %d", $pid)); } $patch += array( 'module' => '', 'description' => '', 'url' => '', 'status' => 'open', ); $form = array(); $form['patch'] = array( '#type' => 'fieldset', '#title' => t('Add patch record'), '#tree' => TRUE, ); $form['patch']['module'] = array( '#type' => 'select', '#title' => t('Affected modules'), '#options' => module_list(FALSE, FALSE, TRUE), '#multiple' => TRUE, '#default_value' => explode(',', $patch['module']), '#size' => 8, '#required' => TRUE, '#prefix' => '
    ', '#suffix' => '
    ', ); $form['patch']['description'] = array( '#type' => 'textarea', '#title' => t('Description'), '#default_value' => $patch['description'], '#required' => TRUE, '#prefix' => '
    ', '#suffix' => '
    ', ); $form['patch']['url'] = array( '#type' => 'textfield', '#title' => t('Issue URL'), '#default_value' => $patch['url'], '#prefix' => '
    ', ); $form['patch']['status'] = array( '#type' => 'select', '#title' => t('Status'), '#options' => array('open' => t('open'), 'fixed' => t('fixed'), "won't fix" => t("won't fix")), '#default_value' => $patch['status'], '#suffix' => '
    ', ); if (!empty($patch['pid'])) { $form['patch']['pid'] = array( '#type' => 'value', '#value' => $patch['pid'], ); } $form['patch']['submit'] = array( '#type' => 'submit', '#value' => isset($patch['pid']) ? t('Save') : t('Add'), ); if (!empty($patch['pid'])) { $form['patch']['delete'] = array( '#type' => 'submit', '#value' => t('Delete'), ); } return $form; } /** * Submit handler for journal patch form. */ function journal_patch_form_submit($form_id, $form_values) { global $user; $patch = $form_values['patch']; if (preg_match('@^#\d+@', $patch['url'])) { $patch['url'] = 'http://drupal.org/node/'. substr($patch['url'], 1); } if ($form_values['op'] == t('Add')) { $pid = db_next_id("{journal_patch}_pid"); db_query("INSERT INTO {journal_patch} (pid, uid, module, description, url, status, timestamp) VALUES (%d, %d, '%s', '%s', '%s', '%s', %d)", $pid, $user->uid, implode(',', $patch['module']), $patch['description'], $patch['url'], $patch['status'], time()); } else if ($form_values['op'] == t('Save')) { db_query("UPDATE {journal_patch} SET uid = %d, module = '%s', description = '%s', url = '%s', status = '%s' WHERE pid = %d", $user->uid, implode(',', $patch['module']), $patch['description'], $patch['url'], $patch['status'], $patch['pid']); return 'admin/logs/journal/patches'; } else if ($form_values['op'] == t('Delete')) { return 'admin/logs/journal/patches/delete/'. $patch['pid']; } } /** * Confirmation form to delete a patch record. */ function journal_patch_delete_confirm($pid) { $form = array(); $form['pid'] = array('#type' => 'value', '#value' => $pid); $description = db_result(db_query("SELECT description FROM {journal_patch} WHERE pid = %d", $pid)); $form['patch_description'] = array( '#type' => 'item', '#title' => t('Description'), '#value' => filter_xss_admin($description), ); return confirm_form($form, t('Are you sure you want to delete this patch record?'), 'admin/logs/journal/patches', t('This action cannot be undone.'), t('Delete'), t('Cancel') ); } /** * Form submit callback for patch record delete confirm form. */ function journal_patch_delete_confirm_submit($form, &$form_values) { db_query("DELETE FROM {journal_patch} WHERE pid = %d", $form_values['pid']); return 'admin/logs/journal/patches'; }