diff --git a/parsely.admin.inc b/parsely.admin.inc index 38f15c7..fa0db3d 100644 --- a/parsely.admin.inc +++ b/parsely.admin.inc @@ -19,13 +19,36 @@ function parsely_admin_settings($form, &$form_state) { ); if (module_exists('taxonomy')) { - $form['parsely_section_vocabulary'] = array( + $form['parsely_sections_wrapper'] = array( + '#type' => 'fieldset', + '#title' => t('Section Settings'), + '#collapsible' => FALSE, + '#collapsed' => FALSE, + ); + $form['parsely_sections_wrapper']['parsely_section_vocabulary'] = array( '#type' => 'radios', '#title' => t('Section Vocabulary'), '#options' => _parsely_vocab_array_format(taxonomy_get_vocabularies()), - '#description' => t('Select the taxonomy to use for Parse.ly sections. The first term in this vocabulary is used to populate the Parse.ly section variable.'), + '#description' => t('Select the taxonomy vocabulary to use for Parse.ly sections. A single term from this vocabulary will be chosen for each tracked node, where applicable, using the criterion specified below.'), '#default_value' => variable_get('parsely_section_vocabulary', ''), ); + $form['parsely_sections_wrapper']['parsely_section_term_criterion'] = array( + '#title' => t('Section Term Selection Criteria'), + '#type' => 'radios', + '#options' => array( + 'first' => t('First term'), + 'last' => t('Last term'), + 'highest' => t('Highest-level term'), + 'ancestor' => t('Highest-level ancestor'), + ), + // Provide a long-winded explanation of how this impacts term selection. + // Note that there's currently no way to say "find the last/newest term, + // and then select its highest-level ancestor" or "use the term that + // appears first in the set of terms for a given term/entity reference + // field," etc. + '#description' => t('If a given node may be associated with multiple terms from the vocabulary you selected above, this setting can help determine which term to use. "First term" and "Last term" will choose a term based on the date/time the term was created (this may differ from the relative position of a term within a term reference field). "Highest term" will choose the term that lives closest to the root of the vocabulary (defaulting to the "first"/oldest of such terms if more than one exist). For hierarchical vocabularies, you can also opt to use the "Highest-level ancestor" of a given term, which means that if a given node is tagged with a lower-lever term (e.g. corresponding to a subsection of your site), we will report that term\'s parent (or grandparent, etc.) term as the section. Wherever a choice must be made between terms at the same depth, the first term will be chosen.'), + '#default_value' => variable_get('parsely_section_term_criterion', 'first'), + ); $form['parsely_tag_vocabularies'] = array( '#type' => 'checkboxes', @@ -37,6 +60,61 @@ function parsely_admin_settings($form, &$form_state) { } // TODO: Should display warning if taxonomy module isn't available + // Optional settings + $form['parsely_metadata'] = array( + '#type' => 'fieldset', + '#title' => t('Metadata Settings'), + '#description' => t('Customize the values reported to Parse.ly for each page. Read more about Parse.ly metadata !here.', array('!here' => l('here', 'http://www.parsely.com/docs/integration/metadata/ppage.html'))), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + // Thumbnail image. + $logo_url = ltrim(theme_get_setting('logo', variable_get('theme_default')), '/'); + $logo_url = url($logo_url, array('absolute' => TRUE)); + $form['parsely_metadata']['parsely_metadata_thumbnail_url'] = array( + '#type' => 'textfield', + '#title' => t('thumbnailUrl'), + '#description' => t('An image that will be used to illustrate a page/post in certain Parse.ly dashboard displays.'), + '#default_value' => variable_get('parsely_metadata_thumbnail_url', $logo_url), + '#maxlength' => 1024, + ); + if (module_exists('token')) { + $form['parsely_metadata']['#description'] .= ' ' . t('You can use tokens from the list below to specify dynamic patterns for each metadata item.'); + // Set up a token browser so admins can use relevant tokens to define + // metadata values. Update all textfields in this fieldset accordingly. + // Note: The ability to provide token_types param to theme_token_tree_link + // was introduced by the token module on 2014-06-19 in v. 7.x-1.5+4-dev. + // @see https://www.drupal.org/node/2289203. + $token_module_info = system_get_info('module', 'token'); + $token_version = $token_module_info['version']; + if (version_compare($token_version, '7.x-1.5+4-dev', 'ge')) { + $theme_function = 'token_tree_link'; + } + else { + $theme_function = 'token_tree'; + } + $form['parsely_metadata']['tokens'] = array( + '#theme' => $theme_function, + '#token_types' => array('node'), + '#global_types' => TRUE, + '#click_insert' => TRUE, + ); + + foreach (element_children($form['parsely_metadata']) as $field_key) { + $field = $form['parsely_metadata'][$field_key]; + if ($field['#type'] == 'textfield') { + // Note: because we're not requiring a minimum number of tokens for + // these fields, the only way they can fail validation is if they + // include a real token that belongs to a disallowed context. + $field['#element_validate'] = array( + 'token_element_validate', + ); + $field['#token_types'] = array('node'); + $form['parsely_metadata'][$field_key] = $field; + } + } + } + // Optional settings $form['parsely_optional_settings'] = array( '#type' => 'fieldset', diff --git a/parsely.module b/parsely.module index 56e798b..1d27c36 100644 --- a/parsely.module +++ b/parsely.module @@ -68,16 +68,17 @@ function parsely_permission() { * are authenticated here. */ function parsely_should_add_metas() { + // @todo: This needs to work on non-node pages. $node = menu_get_object(); if ($node === NULL) { return FALSE; } - if (empty(variable_get('parsely_apikey', ''))) { + $parsely_api_key = variable_get('parsely_apikey', ''); + if (empty($parsely_api_key)) { return FALSE; } - $path = drupal_strtolower(drupal_get_path_alias($_GET['q'])); if (drupal_match_path($path, implode("\r", parsely_paths_to_ignore()))) { return FALSE; @@ -169,7 +170,10 @@ function _parsely_get_canonical_url($node) { } function _parsely_get_authors($node) { - return array($node->name); + $authors = array($node->name); + drupal_alter('parsely_authors', $authors, $node); + + return $authors; } function _parsely_get_pub_date_utc($node) { @@ -188,55 +192,111 @@ function _parsely_get_pub_date_utc($node) { } /** - * Section is the first active term from the vocabulary we've been told to use - * for sections. + * Attempt to determine the "section" to which a given node belongs. */ function _parsely_get_section($node) { - $section_vocabulary = variable_get('parsely_section_vocabulary'); - if (!module_exists('taxonomy') || $section_vocabulary === NULL) - return 'Uncategorized'; - - $active_terms_query = db_select('taxonomy_index', 't') - ->fields('t') - ->condition('nid', $node->nid, '=') - ->execute(); + $section_name = 'Uncategorized'; + $section_term = NULL; + $section_vocabulary_id = variable_get('parsely_section_vocabulary'); + + if (module_exists('taxonomy') && !is_null($section_vocabulary_id)) { + // Find the most relevant term from the designated vocab, using the + // criterion specified in the module configuration. See + // parsely_admin_settings() for an explanation of the supported criteria. + $selection_criterion = variable_get('parsely_section_term_criterion', 'first'); + + $section_term_query = db_select('taxonomy_index', 'ti'); + $section_term_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); + $section_term_query->fields('ttd', array('tid')) + ->condition('ti.nid', $node->nid) + ->condition('ttd.vid', $section_vocabulary_id); + + // "First term" or "Last term." + if ($selection_criterion == 'first' || $selection_criterion == 'last') { + $sort_direction = ($selection_criterion == 'first') ? 'ASC' : 'DESC'; + $section_term_query->orderBy('ttd.tid', $sort_direction) + ->range(0, 1); + $section_term_result = $section_term_query->execute(); + + if ($section_term_result->rowCount() > 0) { + $section_tid = $section_term_result->fetchField(); + $section_term = taxonomy_term_load($section_tid); + } + } + // "Highest-level term." + elseif ($selection_criterion == 'highest') { + $all_terms_result = $section_term_query->orderBy('ttd.tid') + ->execute(); + + if ($all_terms_result->rowCount() > 0) { + $tids = $all_terms_result->fetchCol(); + $terms = taxonomy_term_load_multiple($tids); + // Examine each term and see how many parents it has. We could use + // taxonomy_get_tree() to get the depths of every term in the vocab, + // but that could be problematic for large vocabs. + foreach ($terms as $term) { + // Calculate depth by repeatedly calling taxonomy_get_parents(). This + // is preferable to calling taxonomy_get_parents_all(), because that + // would include all parents of any multi-parent terms, skewing our + // generation count. Note that $depth will be "1" for a top-level term + // here instead of the conventional "0." + $t = $term; + $depth = 0; + while (!is_null($t)) { + $depth++; + $parents = taxonomy_get_parents($t->tid); + $t = array_pop($parents); + } + if (!isset($highest_term) || $depth < $highest_term->depth) { + $term->depth = $depth; + $highest_term = $term; + } + } + $section_term = $highest_term; + } + } + // "Highest-level ancestor." + elseif ($selection_criterion == 'ancestor') { + $first_term_result = $section_term_query->orderBy('ti.created') + ->range(0, 1) + ->execute(); + + if ($first_term_result->rowCount() > 0) { + $first_term_tid = $first_term_result->fetchField(); + $lineage = taxonomy_get_parents_all($first_term_tid); + $section_term = array_pop($lineage); + } + } + } - $row = $active_terms_query->fetchAssoc(); - if (isset($row['tid'])) { - $term = taxonomy_term_load($row['tid']); - return $term->name; - } else { - return 'Uncategorized'; + if (property_exists($section_term, 'name')) { + $section_name = $section_term->name; } + + // Allow any module to alter the section name. + drupal_alter('parsely_section', $section_name, $node, $section_term); + + return $section_name; } function _parsely_get_tags($node) { $supported_vocabularies = variable_get('parsely_tag_vocabularies'); - if (!module_exists('taxonomy') || $supported_vocabularies === NULL) + if (!module_exists('taxonomy') || $supported_vocabularies === NULL) { return array(); - - // Get a mapping of all the terms in this vocabulary so that we can look up - // the name of the active terms by term id - $_terms = array(); - foreach ($supported_vocabularies as $vid) { - $_terms = array_merge($_terms, taxonomy_get_tree($vid)); - } - $terms = array(); - foreach ($_terms as $term) { - $terms[$term->tid] = $term->name; } - // Check what vocabulary terms are on the page that is loading - $active_terms_query = db_select('taxonomy_index', 't') - ->fields('t') - ->condition('nid', $node->nid, '=') + // Fetch the names of all terms that (a) belong to one of the supported + // vocabularies and (b) are associated with the given node. + $active_terms_query = db_select('taxonomy_index', 'ti'); + $active_terms_query->join('taxonomy_term_data', 'ttd', 'ti.tid=ttd.tid'); + $active_terms_result = $active_terms_query->fields('ttd', array('name')) + ->condition('ti.nid', $node->nid) + ->condition('ttd.vid', $supported_vocabularies, 'IN') ->execute(); - // Build up and return active_terms $active_terms = array(); - while ($row = $active_terms_query->fetchAssoc()) { - if (isset($row['tid'])) - array_push($active_terms, $terms[$row['tid']]); + if ($active_terms_result->rowCount() > 0) { + $active_terms = $active_terms_result->fetchCol(); } return $active_terms; @@ -245,22 +305,36 @@ function _parsely_get_tags($node) { function _parsely_get_node_metadata() { $node = menu_get_object(); + // @todo: Integrate with metatag module and/or support more tokens, etc. $parsely_meta = array( '@context' => 'http://schema.org', '@type' => 'WebPage', 'articleId' => _parsely_get_content_id($node), 'headline' => $node->title, 'url' => _parsely_get_canonical_url($node), - 'thumbnailUrl' => 'FIX ME', 'dateCreated' => _parsely_get_pub_date_utc($node), 'articleSection' => _parsely_get_section($node), 'creator' => _parsely_get_authors($node), 'keywords' => _parsely_get_tags($node), ); + if ($thumbnail_url = variable_get('parsely_metadata_thumbnail_url', FALSE)) { + $thumbnail_url = token_replace($thumbnail_url, array('node' => $node)); + if (!empty($thumbnail_url)) { + $parsely_meta['thumbnailUrl'] = $thumbnail_url; + } + } + + // @todo: Allow admins to customize the @type value. if ($node->type === 'article') { $parsely_meta['@type'] = 'NewsArticle'; } + // @todo: Also support the "metadata" key so users can pass in any additional custom data. + + // Allow modules to alter the default metadata for this node. + // Should this be more generic (rather than node-specific)? + drupal_alter('parsely_node_metadata', $parsely_meta, $node); + return $parsely_meta; }