metatag.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <?php
  2. /**
  3. * @file
  4. * Metatag primary classes.
  5. */
  6. interface DrupalMetaTagInterface {
  7. /**
  8. * Constructor
  9. *
  10. * @param array $info
  11. * The information about the meta tag from metatag_get_info().
  12. */
  13. function __construct(array $info, array $data = array());
  14. function getForm();
  15. //function validateForm();
  16. //function processForm();
  17. function getValue();
  18. function getWeight();
  19. function getElement();
  20. function tidyValue($value);
  21. function convertUrlToAbsolute($url);
  22. }
  23. class DrupalDefaultMetaTag implements DrupalMetaTagInterface {
  24. protected $info;
  25. protected $data = array('value' => '');
  26. protected $weight = 0;
  27. function __construct(array $info, array $data = NULL) {
  28. $this->info = $info;
  29. if (isset($data)) {
  30. $this->data = $data;
  31. }
  32. }
  33. /**
  34. * Calculate the weight of this meta tag.
  35. *
  36. * @return integer
  37. */
  38. public function getWeight() {
  39. static $counter = 0;
  40. // If no weight value is found, stack this meta tag at the end.
  41. $weight = 100;
  42. if (!empty($this->info['weight'])) {
  43. $weight = $this->info['weight'];
  44. }
  45. return $weight + ($counter++ * 0.1);
  46. }
  47. /**
  48. * Build the form for this meta tag.
  49. *
  50. * @return array
  51. * A standard FormAPI array.
  52. */
  53. public function getForm(array $options = array()) {
  54. return array();
  55. }
  56. /**
  57. * Get the string value of this meta tag.
  58. *
  59. * @return string
  60. * The value of this meta tag.
  61. */
  62. public function getValue(array $options = array()) {
  63. $value = $this->tidyValue($this->data['value']);
  64. // Translate the final output string prior to output. Use the
  65. // 'output' i18n_string object type, and pass along the meta tag's
  66. // options as the context so it can be handled appropriately.
  67. $value = metatag_translate_metatag($value, $this->info['name'], $options, NULL, TRUE);
  68. return $value;
  69. }
  70. /**
  71. * Get the HTML tag for this meta tag.
  72. *
  73. * @return array
  74. * A render array for this meta tag.
  75. */
  76. public function getElement(array $options = array()) {
  77. $value = $this->getValue($options);
  78. if (strlen($value) === 0) {
  79. return array();
  80. }
  81. // The stack of elements that will be output.
  82. $elements = array();
  83. // Dynamically add each option to this setting.
  84. $base_element = isset($this->info['element']) ? $this->info['element'] : array();
  85. // Single item.
  86. if (empty($this->info['multiple'])) {
  87. $values = array($value);
  88. }
  89. // Multiple items.
  90. else {
  91. $values = array_filter(explode(',', $value));
  92. }
  93. // Loop over each item.
  94. if (!empty($values)) {
  95. foreach ($values as $ctr => $value) {
  96. $value = trim($value);
  97. // Some meta tags must be output as secure URLs.
  98. if (!empty($this->info['secure'])) {
  99. $value = str_replace('http://', 'https://', $value);
  100. }
  101. // Combine the base configuration for this meta tag with the value.
  102. $element = $base_element + array(
  103. '#theme' => 'metatag',
  104. '#tag' => 'meta',
  105. '#id' => 'metatag_' . $this->info['name'] . '_' . $ctr,
  106. '#name' => $this->info['name'],
  107. '#value' => $value,
  108. '#weight' => $this->getWeight(),
  109. );
  110. // Add header information if desired.
  111. if (!empty($this->info['header'])) {
  112. $element['#attached']['drupal_add_http_header'][] = array($this->info['header'], $value);
  113. }
  114. $elements[] = array($element, $element['#id']);
  115. }
  116. }
  117. if (!empty($elements)) {
  118. return array(
  119. '#attached' => array('drupal_add_html_head' => $elements),
  120. );
  121. }
  122. }
  123. /**
  124. * Remove unwanted formatting from a meta tag.
  125. *
  126. * @param $value string
  127. * The meta tag value to be tidied up.
  128. *
  129. * @return string
  130. * The meta tag value after it has been tidied up.
  131. */
  132. public function tidyValue($value) {
  133. // Check for Media strings from the WYSIWYG submodule.
  134. if (module_exists('media_wysiwyg') && strpos($value, '[[{') !== FALSE) {
  135. // In https://www.drupal.org/node/2129273 media_wysiwyg_filter() was
  136. // changed to require several additional arguments.
  137. $langcode = language_default('language');
  138. $value = media_wysiwyg_filter($value, NULL, NULL, $langcode, NULL, NULL);
  139. }
  140. // Specifically replace encoded spaces, because some WYSIWYG editors are
  141. // silly. Do this before decoding the other HTML entities so that the output
  142. // doesn't end up with a bunch of a-circumflex characters.
  143. $value = str_replace('&nbsp;', ' ', $value);
  144. // Decode HTML entities.
  145. $value = decode_entities($value);
  146. // Remove any HTML code that might have been included.
  147. $value = strip_tags($value);
  148. // Strip errant whitespace.
  149. $value = str_replace(array("\r\n", "\n", "\r", "\t"), ' ', $value);
  150. $value = str_replace(' ', ' ', $value);
  151. $value = str_replace(' ', ' ', $value);
  152. $value = trim($value);
  153. return $value;
  154. }
  155. /**
  156. * Make sure a given URL is absolute.
  157. *
  158. * @param string $url
  159. * The URL to convert to an absolute URL.
  160. *
  161. * @return string
  162. * The argument converted to an absolute URL.
  163. */
  164. function convertUrlToAbsolute($url) {
  165. // Convert paths relative to the hostname, that start with a slash, to
  166. // ones that are relative to the Drupal root path; ignore protocol-relative
  167. // URLs.
  168. if (strpos($url, base_path()) === 0 && strpos($url, '//') !== 0) {
  169. // Logic:
  170. // * Get the length of the base_path(),
  171. // * Get a portion of the image's path starting from the position equal
  172. // to the base_path()'s length; this will result in a path relative
  173. // to the Drupal installation's base directory.
  174. $len = strlen(base_path());
  175. $url = substr($url, $len);
  176. }
  177. // Pass everything else through file_create_url(). The alternative is to
  178. // use url() but it would insert '?q=' into the path.
  179. return file_create_url($url);
  180. }
  181. }
  182. /**
  183. * Text-based meta tag controller.
  184. */
  185. class DrupalTextMetaTag extends DrupalDefaultMetaTag {
  186. /**
  187. * {@inheritdoc}
  188. */
  189. public function getForm(array $options = array()) {
  190. $options += array(
  191. 'token types' => array(),
  192. );
  193. $form['value'] = isset($this->info['form']) ? $this->info['form'] : array();
  194. $form['value'] += array(
  195. '#type' => 'textfield',
  196. '#title' => $this->info['label'],
  197. '#description' => !empty($this->info['description']) ? $this->info['description'] : '',
  198. '#default_value' => isset($this->data['value']) ? $this->data['value'] : '',
  199. '#element_validate' => array('token_element_validate'),
  200. '#token_types' => $options['token types'],
  201. '#maxlength' => 1024,
  202. );
  203. // Optional handling for items that allow multiple values.
  204. if (!empty($this->info['multiple'])) {
  205. $form['value']['#description'] .= ' ' . t('Multiple values may be used, separated by a comma. Note: Tokens that return multiple values will be handled automatically.');
  206. }
  207. // Optional handling for images.
  208. if (!empty($this->info['image'])) {
  209. $form['value']['#description'] .= ' ' . t('This will be able to extract the URL from an image field.');
  210. }
  211. // Optional handling for languages.
  212. if (!empty($this->info['is_language'])) {
  213. $form['value']['#description'] .= ' ' . t('This will not be displayed if it is set to the "Language neutral" (i.e. "und").');
  214. }
  215. // Optional support for select_or_other.
  216. if ($form['value']['#type'] == 'select' && !empty($this->info['select_or_other']) && module_exists('select_or_other')) {
  217. $form['value']['#type'] = 'select_or_other';
  218. $form['value']['#other'] = t('Other (please type a value)');
  219. $form['value']['#multiple'] = FALSE;
  220. $form['value']['#other_unknown_defaults'] = 'other';
  221. $form['value']['#other_delimiter'] = FALSE;
  222. $form['value']['#theme'] = 'select_or_other';
  223. $form['value']['#select_type'] = 'select';
  224. $form['value']['#element_validate'] = array('select_or_other_element_validate');
  225. }
  226. // Support for dependencies, using Form API's #states system.
  227. // @see metatag.api.php.
  228. // @see https://api.drupal.org/drupal_process_states
  229. if (!empty($this->info['dependencies'])) {
  230. foreach ($this->info['dependencies'] as $specs) {
  231. $form['value']['#states']['visible'][':input[name*="[' . $specs['dependency'] . '][' . $specs['attribute'] . ']"]'] = array(
  232. $specs['condition'] => $specs['value'],
  233. );
  234. }
  235. }
  236. return $form;
  237. }
  238. /**
  239. * {@inheritdoc}
  240. */
  241. public function getValue(array $options = array()) {
  242. $options += array(
  243. 'instance' => '',
  244. 'token data' => array(),
  245. // Remove any remaining token after the string is parsed.
  246. 'clear' => TRUE,
  247. 'sanitize' => variable_get('metatag_token_sanitize', FALSE),
  248. 'raw' => FALSE,
  249. );
  250. $value = $this->data['value'];
  251. if (empty($options['raw'])) {
  252. // Give other modules the opportunity to use hook_metatag_pattern_alter()
  253. // to modify defined token patterns and values before replacement.
  254. drupal_alter('metatag_pattern', $value, $options['token data'], $this->info['name']);
  255. $value = token_replace($value, $options['token data'], $options);
  256. }
  257. // Special handling for language meta tags.
  258. if (!empty($this->info['is_language'])) {
  259. // If the meta tag value equals LANGUAGE_NONE, i.e. "und", then don't
  260. // output it.
  261. if (is_string($value) && $value == LANGUAGE_NONE) {
  262. $value = '';
  263. }
  264. }
  265. // Special handling for images and other URLs.
  266. if (!empty($this->info['image']) || !empty($this->info['url'])) {
  267. // Support multiple items, whether it's needed or not. Also remove the
  268. // empty values.
  269. $values = array_filter(explode(',', $value));
  270. // If this meta tag does *not* allow multiple items, only keep the first
  271. // one.
  272. if (empty($this->info['multiple']) && !empty($values[0])) {
  273. $values = array($values[0]);
  274. }
  275. foreach ($values as $key => &$image_value) {
  276. // Remove any unwanted whitespace around the value.
  277. $image_value = trim($image_value);
  278. // If this contains embedded image tags, extract the image URLs.
  279. if (!empty($this->info['image']) && strip_tags($image_value) != $image_value) {
  280. $matches = array();
  281. preg_match('/src="([^"]*)"/', $image_value, $matches);
  282. if (!empty($matches[1])) {
  283. $image_value = $matches[1];
  284. }
  285. }
  286. // Convert the URL to an absolute URL.
  287. $image_value = $this->convertUrlToAbsolute($image_value);
  288. // Replace spaces the URL encoded entity to avoid validation problems.
  289. $image_value = str_replace(' ', '%20', $image_value);
  290. }
  291. // Combine the multiple values into a single string.
  292. $value = implode(',', $values);
  293. }
  294. $value = $this->tidyValue($value);
  295. // Translate the final output string prior to output. Use the
  296. // 'output' i18n_string object type, and pass along the meta tag's
  297. // options as the context so it can be handled appropriately.
  298. $value = metatag_translate_metatag($value, $this->info['name'], $options, NULL, TRUE);
  299. return $value;
  300. }
  301. }
  302. /**
  303. * Link type meta tag controller.
  304. */
  305. class DrupalLinkMetaTag extends DrupalTextMetaTag {
  306. /**
  307. * {@inheritdoc}
  308. */
  309. public function getElement(array $options = array()) {
  310. $element = isset($this->info['element']) ? $this->info['element'] : array();
  311. $value = $this->getValue($options);
  312. if (strlen($value) === 0) {
  313. return array();
  314. }
  315. $element += array(
  316. '#theme' => 'metatag_link_rel',
  317. '#tag' => 'link',
  318. '#id' => 'metatag_' . $this->info['name'],
  319. '#name' => $this->info['name'],
  320. '#value' => $value,
  321. '#weight' => $this->getWeight(),
  322. );
  323. if (!isset($this->info['header']) || !empty($this->info['header'])) {
  324. // Also send the generator in the HTTP header.
  325. // @todo This does not support 'rev' or alternate link headers.
  326. $element['#attached']['drupal_add_http_header'][] = array('Link', '<' . $value . '>;' . drupal_http_header_attributes(array('rel' => $element['#name'])), TRUE);
  327. }
  328. return array(
  329. '#attached' => array('drupal_add_html_head' => array(array($element, $element['#id']))),
  330. );
  331. }
  332. }
  333. /**
  334. * Title meta tag controller.
  335. *
  336. * This extends DrupalTextMetaTag as we need to alter variables in
  337. * template_preprocess_html() rather output a normal meta tag.
  338. */
  339. class DrupalTitleMetaTag extends DrupalTextMetaTag {
  340. /**
  341. * {@inheritdoc}
  342. */
  343. public function getElement(array $options = array()) {
  344. $element = array();
  345. if ($value = $this->getValue($options)) {
  346. $element['#attached']['metatag_set_preprocess_variable'][] = array('html', 'head_title', $value);
  347. $element['#attached']['metatag_set_preprocess_variable'][] = array('html', 'head_array', array('title' => $value));
  348. }
  349. return $element;
  350. }
  351. }
  352. /**
  353. * Multiple value meta tag controller.
  354. */
  355. class DrupalListMetaTag extends DrupalDefaultMetaTag {
  356. /**
  357. * {@inheritdoc}
  358. */
  359. function __construct(array $info, array $data = NULL) {
  360. // Ensure that the $data['value] argument is an array.
  361. if (empty($data['value'])) {
  362. $data['value'] = array();
  363. }
  364. $data['value'] = (array) $data['value'];
  365. parent::__construct($info, $data);
  366. }
  367. /**
  368. * {@inheritdoc}
  369. */
  370. public function getForm(array $options = array()) {
  371. $form['value'] = isset($this->info['form']) ? $this->info['form'] : array();
  372. $form['value'] += array(
  373. '#type' => 'checkboxes',
  374. '#title' => $this->info['label'],
  375. '#description' => !empty($this->info['description']) ? $this->info['description'] : '',
  376. '#default_value' => isset($this->data['value']) ? $this->data['value'] : array(),
  377. );
  378. return $form;
  379. }
  380. /**
  381. * {@inheritdoc}
  382. */
  383. public function getValue(array $options = array()) {
  384. $values = array_keys(array_filter($this->data['value']));
  385. sort($values);
  386. $value = implode(', ', $values);
  387. $value = $this->tidyValue($value);
  388. // Translate the final output string prior to output. Use the
  389. // 'output' i18n_string object type, and pass along the meta tag's
  390. // options as the context so it can be handled appropriately.
  391. $value = metatag_translate_metatag($value, $this->info['name'], $options, NULL, TRUE);
  392. return $value;
  393. }
  394. }
  395. /**
  396. * Date interval meta tag controller.
  397. */
  398. class DrupalDateIntervalMetaTag extends DrupalDefaultMetaTag {
  399. /**
  400. * {@inheritdoc}
  401. */
  402. public function getForm(array $options = array()) {
  403. $form['value'] = array(
  404. '#type' => 'textfield',
  405. '#title' => t('!title interval', array('!title' => $this->info['label'])),
  406. '#default_value' => isset($this->data['value']) ? $this->data['value'] : '',
  407. '#element_validate' => array('element_validate_integer_positive'),
  408. '#maxlength' => 4,
  409. '#description' => isset($this->info['description']) ? $this->info['description'] : '',
  410. );
  411. $form['period'] = array(
  412. '#type' => 'select',
  413. '#title' => t('!title interval type', array('!title' => $this->info['label'])),
  414. '#default_value' => isset($this->data['period']) ? $this->data['period'] : '',
  415. '#options' => array(
  416. '' => t('- none -'),
  417. 'day' => t('Day(s)'),
  418. 'week' => t('Week(s)'),
  419. 'month' => t('Month(s)'),
  420. 'year' => t('Year(s)'),
  421. ),
  422. );
  423. return $form;
  424. }
  425. /**
  426. * {@inheritdoc}
  427. */
  428. public function getValue(array $options = array()) {
  429. $value = '';
  430. if (!empty($this->data['value'])) {
  431. $interval = intval($this->data['value']);
  432. if (!empty($interval) && !empty($this->data['period'])) {
  433. $period = $this->data['period'];
  434. $value = format_plural($interval, '@count ' . $period, '@count ' . $period . 's');
  435. }
  436. }
  437. // Translate the final output string prior to output. Use the
  438. // 'output' i18n_string object type, and pass along the meta tag's
  439. // options as the context so it can be handled appropriately.
  440. $value = metatag_translate_metatag($value, $this->info['name'], $options, NULL, TRUE);
  441. return $value;
  442. }
  443. }