diff --git a/modules/page_title_path/page_title_path.admin.inc b/modules/page_title_path/page_title_path.admin.inc
new file mode 100644
index 0000000..27999b1
--- /dev/null
+++ b/modules/page_title_path/page_title_path.admin.inc
@@ -0,0 +1,498 @@
+ array(
+ 'data' => t('Path'),
+ 'field' => 'path',
+ 'sort' => 'asc',
+ ),
+ 'page_title' => array(
+ 'data' => t('Page title'),
+ 'field' => 'page_title',
+ ),
+ 'operations' => array(
+ 'data' => t('Operations'),
+ ),
+ );
+
+ // Get filter keys and add the filter form.
+ $keys = func_get_args();
+ $keys = array_splice($keys, 2); // Offset the $form and $form_state parameters.
+ $keys = implode('/', $keys);
+ $form['page_title_path_list_filter_form'] = page_title_path_list_filter_form($keys);
+
+ // Build the 'Update options' form.
+ $form['operations'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Update options'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ $operations = array();
+ foreach ($form['#operations'] as $key => $operation) {
+ $operations[$key] = $operation['action'];
+ }
+ $form['operations']['operation'] = array(
+ '#type' => 'select',
+ '#options' => $operations,
+ '#default_value' => 'delete',
+ );
+ $form['operations']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Update'),
+ '#validate' => array('page_title_path_list_form_operations_validate'),
+ '#submit' => array('page_title_path_list_form_operations_submit'),
+ );
+
+ // Building the SQL query and load the paths.
+ $query = db_select('page_title_path', 'ptp')
+ ->extend('TableSort')
+ ->extend('PagerDefault');
+ $query->join('page_title', 'pt', 'ptp.id = pt.id');
+ $query->addField('ptp', 'id');
+ $query->condition('pt.type', 'path');
+ $query->orderByHeader($header);
+ $query->limit(50);
+ $query->addTag('page_title_path_list');
+ page_title_path_build_filter_query($query, array('ptp.path', 'pt.page_title'), $keys);
+ $ids = $query->execute()->fetchCol();
+ $page_titles = page_title_path_load_multiple($ids);
+
+ $rows = array();
+ foreach ($page_titles as $page_title) {
+ $row = array();
+ $path = check_plain($page_title->path);
+
+ $row['path'] = l($path, $path);
+ $row['page_title'] = check_plain($page_title->page_title);
+ $row['operations'] = array(
+ 'data' => array(
+ '#theme' => 'links',
+ '#links' => array(
+ 'edit' => array(
+ 'title' => t('Edit'),
+ 'href' => 'admin/config/search/page-title/path/edit/' . $page_title->id,
+ 'query' => $destination,
+ ),
+ 'delete' => array(
+ 'title' => t('Delete'),
+ 'href' => 'admin/config/search/page-title/path/delete/' . $page_title->id,
+ 'query' => $destination,
+ ),
+ ),
+ '#attributes' => array(
+ 'class' => array(
+ 'links',
+ 'inline',
+ 'nowrap',
+ ),
+ ),
+ ),
+ );
+
+ $rows[$page_title->id] = $row;
+ }
+
+ $form['ids'] = array(
+ '#type' => 'tableselect',
+ '#header' => $header,
+ '#options' => $rows,
+ '#empty' => t('No page titles available.'),
+ );
+ $form['ids']['#empty'] .= ' ' . l(t('Add path-based page title.'), 'admin/config/search/page-title/path/add', array('query' => $destination));
+ $form['pager'] = array('#theme' => 'pager');
+
+ return $form;
+}
+
+/**
+ * Return a form to filter page title paths.
+ *
+ * @see page_title_path_list_filter_form_submit()
+ *
+ * @ingroup forms
+ */
+function page_title_path_list_filter_form($keys = '') {
+ $form = array();
+
+ $form['#attributes'] = array('class' => array('search-form'));
+ $form['basic'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Filter page title paths'),
+ '#attributes' => array('class' => array('container-inline')),
+ );
+ $form['basic']['filter'] = array(
+ '#type' => 'textfield',
+ '#title' => '',
+ '#default_value' => $keys,
+ '#maxlength' => 128,
+ '#size' => 25,
+ );
+ $form['basic']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Filter'),
+ '#submit' => array('page_title_path_list_filter_form_submit'),
+ );
+ if ($keys) {
+ $form['basic']['reset'] = array(
+ '#type' => 'submit',
+ '#value' => t('Reset'),
+ '#submit' => array('page_title_path_list_filter_form_reset'),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Process filter form submission when the Filter button is pressed.
+ */
+function page_title_path_list_filter_form_submit($form, &$form_state) {
+ $form_state['redirect'] = 'admin/config/search/page-title/path/' . trim($form_state['values']['filter']);
+}
+
+/**
+ * Process filter form submission when the Reset button is pressed.
+ */
+function page_title_path_list_filter_form_reset($form, &$form_state) {
+ $form_state['redirect'] = 'admin/config/search/page-title/path';
+}
+
+/**
+ * Extends a query object for page title path filters.
+ *
+ * @param $query
+ * Query object that should be filtered.
+ * @param $fields
+ * The fields to filter on.
+ * @param $keys
+ * The filter values to use.
+ */
+function page_title_path_build_filter_query(QueryAlterableInterface $query, array $fields, $keys = '') {
+ if ($keys && $fields) {
+ // Replace wildcards with PDO wildcards.
+ $conditions = db_or();
+ $wildcard = '%' . trim(preg_replace('!\*+!', '%', db_like($keys)), '%') . '%';
+ foreach ($fields as $field) {
+ $conditions->condition($field, $wildcard, 'LIKE');
+ }
+ $query->condition($conditions);
+ }
+}
+
+/**
+ * Validate page_title_path_list_form form submissions.
+ *
+ * Check if any page title paths have been selected to perform the chosen
+ * 'Update option' on.
+ */
+function page_title_path_list_form_operations_validate($form, &$form_state) {
+ // Error if there are no page title paths selected.
+ if (!is_array($form_state['values']['ids']) || !count(array_filter($form_state['values']['ids']))) {
+ form_set_error('', t('No paths selected.'));
+ }
+}
+
+/**
+ * Process page_title_path_list_form form submissions.
+ *
+ * Execute the chosen 'Update option' on the selected paths.
+ */
+function page_title_path_list_form_operations_submit($form, &$form_state) {
+ $operations = $form['#operations'];
+ $operation = $operations[$form_state['values']['operation']];
+
+ // Filter out unchecked paths.
+ $ids = array_filter($form_state['values']['ids']);
+
+ if (!empty($operation['confirm']) && empty($form_state['values']['confirm'])) {
+ // We need to rebuild the form to go to a second step. For example, to
+ // show the confirmation form for the deletion of redirects.
+ $form_state['rebuild'] = TRUE;
+ }
+ else {
+ $function = $operation['callback'];
+
+ // Add in callback arguments if present.
+ if (isset($operation['callback arguments'])) {
+ $args = array_merge(array($ids), $operation['callback arguments']);
+ }
+ else {
+ $args = array($ids);
+ }
+ call_user_func_array($function, $args);
+
+ $count = count($form_state['values']['ids']);
+ watchdog('page_title_path', '@action @count paths.', array('@action' => $operation['action_past'], '@count' => $count));
+ drupal_set_message(format_plural(count($ids), '@action @count path.', '@action @count paths.', array('@action' => $operation['action_past'], '@count' => $count)));
+ }
+}
+
+function page_title_path_list_form_operations_confirm_form($form, &$form_state, $operation, $ids) {
+ $operations = $form['#operations'];
+ $operation = $operations[$form_state['values']['operation']];
+
+ $form['ids_list'] = array(
+ '#theme' => 'item_list',
+ '#items' => array(),
+ );
+ $form['ids'] = array(
+ '#type' => 'value',
+ '#value' => $ids,
+ );
+
+ $paths = page_title_path_load_multiple($ids);
+ foreach ($paths as $id => $path) {
+ $form['ids_list']['#items'][$id] = $GLOBALS['base_url'] . '/' . check_plain($path);
+ }
+
+ $form['operation'] = array(
+ '#type' => 'hidden',
+ '#value' => $form_state['values']['operation']
+ );
+ $form['#submit'][] = 'page_title_path_list_form_operations_submit';
+ $confirm_question = format_plural(count($ids), 'Are you sure you want to @action this page title?', 'Are you sure you want to @action these page titles?', array('@action' => drupal_strtolower($operation['action'])));
+
+ return confirm_form(
+ $form,
+ $confirm_question,
+ 'admin/config/search/page-title/', // @todo This does not redirect back to filtered page.
+ t('This action cannot be undone.'),
+ $operation['action'],
+ t('Cancel')
+ );
+}
+
+/**
+ * Form builder to add or edit an URL redirect.
+ *
+ * @see page_title_path_element_validate_path
+ * @see page_title_path_edit_form_validate()
+ * @see page_title_path_edit_form_submit()
+ *
+ * @ingroup forms
+ */
+function page_title_path_edit_form($form, &$form_state, $page_title = NULL) {
+ $form['id'] = array(
+ '#type' => 'value',
+ '#value' => isset($page_title) ? $page_title->id : NULL,
+ );
+ $form['path'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Path'),
+ '#description' => t('Enter an interal Drupal path or path alias.'),
+ '#maxlength' => 255,
+ '#default_value' => isset($page_title) ? $page_title->path : '',
+ '#required' => TRUE,
+ '#disabled' => isset($page_title) ? TRUE : FALSE,
+ '#field_prefix' => $GLOBALS['base_url'] . '/' . (variable_get('clean_url', 0) ? '' : '?q='),
+ '#element_validate' => array('page_title_path_element_validate_path'),
+ );
+ $form['page_title']['page_title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Page title'),
+ '#description' => t('Provide a description of this path to appear in the <title> tag which search engines can use in search result listings (optional). It is generally accepted this field should be less than 70 characters.'),
+ '#default_value' => $page_title->page_title,
+ '#required' => TRUE,
+ '#size' => 60,
+ '#maxlength' => 255,
+ );
+
+ $form['override'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('I understand the following warnings and would like to proceed with saving this page title.'),
+ '#default_value' => FALSE,
+ '#access' => FALSE,
+ '#required' => FALSE,
+ '#weight' => -100,
+ '#prefix' => '',
+ '#suffix' => '
',
+ );
+ if (!empty($form_state['storage']['override_messages'])) {
+ $form['override']['#access'] = TRUE;
+ //$form['override']['#required'] = TRUE;
+ $form['override']['#description'] = theme('item_list', array('items' => $form_state['storage']['override_messages']));
+ // Reset the messages.
+ $form_state['storage']['override_messages'] = array();
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+ $form['actions']['cancel'] = array(
+ '#type' => 'link',
+ '#title' => t('Cancel'),
+ '#href' => isset($_GET['destination']) ? $_GET['destination'] : 'admin/config/search/page-title/path',
+ );
+
+ return $form;
+}
+
+/**
+ * Element validate handler; validate the path.
+ *
+ * @see page_title_path_edit_form()
+ */
+function page_title_path_element_validate_path($element, &$form_state) {
+ $value = &$element['#value'];
+
+ // Check that the source contains no URL fragment.
+ if (strpos($value, '#') !== FALSE) {
+ form_error($element, t('The path cannot contain an URL fragment anchor.'));
+ }
+
+ // Disallow a page title for the frontpage (Page Title takes care of this).
+ if ($value === '') {
+ form_error($element, t('The path cannot be the front page. The front page title can be set on the Page Title settings page.'));
+ }
+
+ // Only allow page titles for valid paths.
+ if (empty($form_state['values']['override'])) {
+ $menu_item = menu_get_item($value);
+ if (!$menu_item) {
+ $form_state['storage']['override_messages']['valid-path'] = t('The path %path does not appear to be a valid interal path.', array('%path' => $value));
+ $form_state['rebuild'] = TRUE;
+ }
+ }
+
+ return $element;
+}
+
+/**
+ * Form validate handler; validate page title path.
+ *
+ * @see page_title_path_edit_form()
+ */
+function page_title_path_edit_form_validate($form, &$form_state) {
+ if (empty($form_state['values']['override'])) {
+ if ($existing = page_title_path_load_by_path($form_state['values']['path'])) {
+ if ($form_state['values']['id'] != $existing->id) {
+ form_set_error('path', t('A page title for path %path already exists. Do you want to edit the existing page title?', array('%path' => $page_title->path, '@edit-page' => url('admin/config/search/page-title/path/edit/'. $existing->id))));
+ }
+ }
+ if ($form['override']['#access']) {
+ drupal_set_message('Did you read the warnings and click the checkbox?', 'error');
+ $form_state['rebuild'] = TRUE;
+ }
+ }
+}
+
+/**
+ * Form submit handler; insert or update a page title.
+ *
+ * @see page_title_path_edit_form()
+ */
+function page_title_path_edit_form_submit($form, &$form_state) {
+ form_state_values_clean($form_state);
+ $page_title = (object) $form_state['values'];
+ page_title_path_save($page_title);
+ drupal_set_message(t('The page title has been saved.'));
+ $form_state['redirect'] = 'admin/config/search/page-title/path';
+}
+
+/**
+ * Form builder to delete an page title.
+ *
+ * @see confirm_form()
+ *
+ * @ingroup forms
+ */
+function page_title_path_delete_form($form, &$form_state, $page_title) {
+ $form['id'] = array(
+ '#type' => 'value',
+ '#value' => $page_title->id,
+ );
+
+ return confirm_form(
+ $form,
+ t('Are you sure you want to delete the page title for %path?', array('%path' => $GLOBALS['base_url'] . '/' . $page_title->path)),
+ 'admin/config/search/page-title/path'
+ );
+}
+
+/**
+ * Form submit handler; delete a page title after confirmation.
+ *
+ * @see page_title_path_delete_form()
+ */
+function page_title_path_delete_form_submit($form, &$form_state) {
+ page_title_path_delete($form_state['values']['id']);
+ drupal_set_message(t('The page title has been deleted.'));
+ $form_state['redirect'] = 'admin/config/search/page-title/path';
+}
+
+function page_title_path_list_table($page_titles, $header) {
+ $destination = drupal_get_destination();
+
+ // Set up the header.
+ $header = array_combine($header, $header);
+ $header = array_intersect_key(array(
+ 'path' => array(
+ 'data' => t('Path'),
+ 'field' => 'path',
+ 'sort' => 'asc',
+ ),
+ 'operations' => array(
+ 'data' => t('Operations'),
+ ),
+ ), $header);
+
+ $rows = array();
+ foreach ($page_titles as $page_title) {
+ $row = array();
+ $row['data']['path'] = l($GLOBALS['base_url'] . '/' . $page_title->path, $page_title->path);
+ $row['data']['operations'] = array(
+ 'data' => array(
+ '#theme' => 'links',
+ '#links' => array(
+ 'edit' => array(
+ 'title' => t('Edit'),
+ 'href' => 'admin/config/search/page-title/path/edit/' . $page_title->id,
+ 'query' => $destination,
+ ),
+ 'delete' => array(
+ 'title' => t('Delete'),
+ 'href' => 'admin/config/search/page-title/path/delete/' . $page_title->id,
+ 'query' => $destination,
+ ),
+ ),
+ '#attributes' => array(
+ 'class' => array(
+ 'links',
+ 'inline',
+ 'nowrap',
+ ),
+ ),
+ ),
+ );
+
+ $row['data'] = array_intersect_key($row['data'], $header);
+ $rows[$page_title->id] = $row;
+ }
+
+ $build['list'] = array(
+ '#theme' => 'table',
+ '#header' => $header,
+ '#rows' => $rows,
+ '#empty' => t('No page titles available.'),
+ '#attributes' => array('class' => array('page-title-list')),
+ );
+
+ return $build;
+}
diff --git a/modules/page_title_path/page_title_path.info b/modules/page_title_path/page_title_path.info
new file mode 100644
index 0000000..7035b01
--- /dev/null
+++ b/modules/page_title_path/page_title_path.info
@@ -0,0 +1,6 @@
+name = Page Title Path
+description = Override the page title (in the <head> tag) by path.
+core = 7.x
+package = SEO
+dependencies[] = page_title
+configure = admin/config/search/page-title/path
diff --git a/modules/page_title_path/page_title_path.install b/modules/page_title_path/page_title_path.install
new file mode 100644
index 0000000..eef2b4a
--- /dev/null
+++ b/modules/page_title_path/page_title_path.install
@@ -0,0 +1,35 @@
+ 'Path based meta tags',
+ 'fields' => array(
+ 'id' => array(
+ 'type' => 'serial',
+ 'not null' => TRUE,
+ 'description' => 'Unique id generated for the path.',
+ ),
+ 'path' => array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'description' => 'The path to apply the page title to.',
+ ),
+ ),
+ 'primary key' => array(
+ 'id',
+ 'path'
+ ),
+ );
+
+ return $schema;
+}
diff --git a/modules/page_title_path/page_title_path.module b/modules/page_title_path/page_title_path.module
new file mode 100644
index 0000000..eaf55b5
--- /dev/null
+++ b/modules/page_title_path/page_title_path.module
@@ -0,0 +1,216 @@
+ 'Path-based Page Titles',
+ 'description' => 'Create path-based page titles.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('page_title_path_list_form'),
+ 'access arguments' => array('administer page titles'),
+ 'file' => 'page_title_path.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ );
+ $items['admin/config/search/page-title/path/add'] = array(
+ 'title' => 'Add path-based page title',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('page_title_path_edit_form'),
+ 'access arguments' => array('administer page titles'),
+ 'file' => 'page_title_path.admin.inc',
+ 'type' => MENU_LOCAL_ACTION,
+ );
+ $items['admin/config/search/page-title/path/edit/%page_title_path'] = array(
+ 'title' => 'Edit path-based page title',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('page_title_path_edit_form', 6),
+ 'access arguments' => array('administer page titles'),
+ 'file' => 'page_title_path.admin.inc',
+ );
+ $items['admin/config/search/page-title/path/delete/%page_title_path'] = array(
+ 'title' => 'Delete path-based page title',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('page_title_path_delete_form', 6),
+ 'access arguments' => array('administer page titles'),
+ 'file' => 'page_title_path.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Saves a page title.
+ *
+ * @param $page_title
+ * The page title object to be saved. If $page_title->id is omitted a new
+ * page_title will be created.
+ *
+ * @ingroup redirect_api
+ */
+function page_title_path_save($page_title) {
+ if (empty($page_title->id)) {
+ $id = db_insert('page_title_path')
+ ->fields(array(
+ 'path' => $page_title->path,
+ ))
+ ->execute();
+ db_insert('page_title')
+ ->fields(array(
+ 'type' => 'path',
+ 'id' => $id,
+ 'page_title' => $page_title->page_title,
+ ))
+ ->execute();
+ }
+ else {
+ db_update('page_title')
+ ->fields(array(
+ 'page_title' => $page_title->page_title,
+ ))
+ ->condition('type', 'path')
+ ->condition('id', $page_title->id)
+ ->execute();
+ }
+}
+
+/**
+ * Loads a single page title path from the database.
+ *
+ * @param $id
+ * A page title path ID.
+ *
+ * @ingroup page_title_path_api
+ */
+function page_title_path_load($id) {
+ return array_shift(page_title_path_load_multiple(array($id)));
+}
+
+/**
+ * Loads multiple page title paths from the database.
+ *
+ * @param $ids
+ * An array of page title path IDs.
+ *
+ * @return
+ * An indexed array of page title objects.
+ *
+ * @ingroup page_title_path_api
+ */
+function page_title_path_load_multiple($ids) {
+ if (is_array($ids) && !empty($ids)) {
+ $select = db_select('page_title', 'pt');
+ $select->join('page_title_path', 'ptp', 'pt.id = ptp.id');
+ $select->fields('ptp');
+ $select->fields('pt', array('page_title'));
+ $select->condition('pt.type', 'path');
+ $select->condition('ptp.id', $ids, 'IN');
+
+ return $select->execute()->fetchAll();
+ }
+}
+
+/**
+ * Loads a single page title path from the database by path.
+ *
+ * @param $path
+ * A page title path.
+ *
+ * @ingroup page_title_path_api
+ */
+function page_title_path_load_by_path($path) {
+ $select = db_select('page_title', 'pt');
+ $select->join('page_title_path', 'ptp', 'pt.id = ptp.id');
+ $select->fields('ptp');
+ $select->fields('pt', array('page_title'));
+ $select->condition('pt.type', 'path');
+ $select->condition('ptp.path', $path);
+
+ return array_shift($select->execute()->fetchAll());
+}
+
+
+/**
+ * Deletes a single page title path from the database.
+ *
+ * @param $id
+ * A page title path ID.
+ *
+ * @ingroup page_title_path_api
+ */
+function page_title_path_delete($id) {
+ page_title_path_delete_multiple(array($id));
+}
+
+/**
+ * Deletes multiple page title paths from the database.
+ *
+ * @param $ids
+ * An array of page title path IDs.
+ *
+ * @return
+ * An array containing the path & page title.
+ *
+ * @ingroup page_title_path_api
+ */
+function page_title_path_delete_multiple($ids) {
+ db_delete('page_title')
+ ->condition('type', 'path')
+ ->condition('id', $ids, 'IN')
+ ->execute();
+ db_delete('page_title_path')
+ ->condition('id', $ids, 'IN')
+ ->execute();
+}
+
+/**
+ * Fetch an array of redirect bulk operations.
+ *
+ * @see hook_redirect_operations()
+ * @see hook_redirect_operations_alter()
+ */
+function page_title_path_get_page_title_path_operations() {
+ $operations = &drupal_static(__FUNCTION__);
+
+ if (!isset($operations)) {
+ $operations = module_invoke_all('page_title_path_operations');
+ drupal_alter('page_title_path_operations', $operations);
+ }
+
+ return $operations;
+}
+
+/**
+ * Implements hook_page_title_path_operations().
+ */
+function page_title_path_page_title_path_operations() {
+ $operations = array();
+
+ $operations['delete'] = array(
+ 'action' => t('Delete'),
+ 'action_past' => t('Deleted'),
+ 'callback' => 'page_title_path_delete_multiple',
+ 'confirm' => TRUE,
+ );
+
+ return $operations;
+}
+
+/**
+ * Implements hook_page_title_alter().
+ */
+function page_title_path_page_title_alter(&$title) {
+ $menu_item = menu_get_item();
+ $page_title = page_title_path_load_by_path($menu_item['path']);
+
+ if ($page_title) {
+ $title = $page_title->page_title;
+ }
+}
diff --git a/page_title.module b/page_title.module
index 1153de5..2408227 100644
--- a/page_title.module
+++ b/page_title.module
@@ -125,6 +125,11 @@ function page_title_menu() {
'type' => MENU_NORMAL_ITEM,
'file' => 'page_title.admin.inc',
);
+ $items['admin/config/search/page-title/settings'] = array(
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'title' => 'Settings',
+ 'weight' => -10,
+ );
return $items;
}