PageRenderTime 46ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/wire/core/MarkupQA.php

http://github.com/ryancramerdesign/ProcessWire
PHP | 270 lines | 191 code | 22 blank | 57 comment | 9 complexity | 6abbffc5b7a2d309343899bc85be8c9c MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception
  1. <?php
  2. /**
  3. * HTML Markup Quality Assurance
  4. *
  5. * Provides runtime quality assurance for markup stored in [textarea] field values.
  6. *
  7. * 1. Ensures URLs referenced in <a> and <img> tags are relative to actual site root.
  8. * 2. Identifies and logs <img> tags that point to non-existing files in PW's file system.
  9. * 3. Re-creates image variations that don't exist, when the original still exists.
  10. * 4. Populates blank 'alt' attributes with actual file description.
  11. *
  12. * - For #1 use the wakeupUrls($value) and sleepUrls($value) methods.
  13. * - For #2-4 use the checkImgTags($value) method.
  14. *
  15. * Runtime errors are logged to: /site/assets/logs/markup-qa-errors.txt
  16. *
  17. * ProcessWire 2.x
  18. * Copyright 2015 by Ryan Cramer
  19. * This file licensed under Mozilla Public License v2.0 http://mozilla.org/MPL/2.0/
  20. *
  21. */
  22. class MarkupQA extends Wire {
  23. const errorLogName = 'markup-qa-errors';
  24. protected $assetsURL = '';
  25. protected $page;
  26. protected $field;
  27. public function __construct(Page $page, Field $field) {
  28. $this->setPage($page);
  29. $this->setField($field);
  30. $this->assetsURL = $this->wire('config')->urls->assets;
  31. }
  32. public function setPage(Page $page) {
  33. $this->page = $page;
  34. }
  35. public function setField(Field $field) {
  36. $this->field = $field;
  37. }
  38. /**
  39. * Wakeup URLs in href or src attributes for presentation
  40. *
  41. * @param $value
  42. *
  43. */
  44. public function wakeupUrls(&$value) {
  45. return $this->checkUrls($value, false);
  46. }
  47. /**
  48. * Sleep URLs in href or src attributes for storage
  49. *
  50. * @param $value
  51. *
  52. */
  53. public function sleepUrls(&$value) {
  54. return $this->checkUrls($value, true);
  55. }
  56. /**
  57. * Wake URLs for presentation or sleep for storage
  58. *
  59. * @param string $value
  60. * @param bool $sleep
  61. *
  62. */
  63. protected function checkUrls(&$value, $sleep = false) {
  64. // see if quick exit possible
  65. if(stripos($value, 'href=') === false && stripos($value, 'src=') === false) return;
  66. $rootURL = $this->wire('config')->urls->root;
  67. $replacements = array(
  68. " href=\"$rootURL" => "\thref=\"/",
  69. " href='$rootURL" => "\thref='/",
  70. " src=\"$rootURL" => "\tsrc=\"/",
  71. " src='$rootURL" => "\tsrc='/",
  72. );
  73. if($sleep) {
  74. // sleep
  75. $value = str_ireplace(array_keys($replacements), array_values($replacements), $value);
  76. } else if(strpos($value, "\t") === false) {
  77. // no wakeup necessary (quick exit)
  78. return;
  79. } else {
  80. // wakeup
  81. $value = str_ireplace(array_values($replacements), array_keys($replacements), $value);
  82. }
  83. }
  84. /**
  85. * Quality assurance for <img> tags
  86. *
  87. * @param string $value
  88. *
  89. */
  90. public function checkImgTags(&$value) {
  91. if(strpos($value, '<img ') !== false && preg_match_all('{(<img [^>]+>)}', $value, $matches)) {
  92. foreach($matches[0] as $key => $img) {
  93. $this->checkImgTag($value, $img);
  94. }
  95. }
  96. }
  97. /**
  98. * Format <img> tag
  99. *
  100. * @param string $value Entire text
  101. * @param string $img Just the found <img> tag
  102. *
  103. */
  104. protected function checkImgTag(&$value, $img) {
  105. $replaceAlt = ''; // exact text to replace for blank alt attribute, i.e. alt=""
  106. $src = '';
  107. $user = $this->wire('user');
  108. $attrStrings = explode(' ', $img); // array of strings like "key=value"
  109. // determine current 'alt' and 'src' attributes
  110. foreach($attrStrings as $n => $attr) {
  111. if(!strpos($attr, '=')) continue;
  112. list($name, $val) = explode('=', $attr);
  113. $name = strtolower($name);
  114. $val = trim($val, "\"' ");
  115. if($name == 'alt' && !strlen($val)) {
  116. $replaceAlt = $attr;
  117. } else if($name == 'src') {
  118. $src = $val;
  119. }
  120. }
  121. // if <img> had no src attr, or if it was pointing to something outside of PW assets, skip it
  122. if(!$src || strpos($src, $this->assetsURL) === false) return;
  123. // recognized site image, make sure the file exists
  124. $pagefile = $this->page->filesManager()->getFile($src);
  125. // if this doesn't resolve to a known pagefile, stop now
  126. if(!$pagefile) {
  127. if(file_exists($this->page->filesManager()->path() . basename($src))) {
  128. // file exists, but we just don't know what it is - leave it alone
  129. } else {
  130. $this->error("Image file on page {$this->page->path} no longer exists (" . basename($src) . ")");
  131. if($this->page->of()) $value = str_replace($img, '', $value);
  132. }
  133. return;
  134. }
  135. if($pagefile->page->id != $this->page->id && !$user->hasPermission('page-view', $pagefile->page)) {
  136. // if the file resolves to another page that the user doesn't have access to view,
  137. // then we will simply remove the image
  138. $this->error("Image referenced on page {$this->page->path} that user does not have view access to ($src)");
  139. if($this->page->of()) $value = str_replace($img, '', $value);
  140. return;
  141. }
  142. if($replaceAlt && $this->page->of()) {
  143. // image has a blank alt tag, meaning, we will auto-populate it with current file description,
  144. // if output formatting is on
  145. $alt = $pagefile->description;
  146. if(strlen($alt)) {
  147. $alt = $this->wire('sanitizer')->entities1($alt);
  148. $_img = str_replace(" $replaceAlt", " alt=\"$alt\"", $img);
  149. $value = str_replace($img, $_img, $value);
  150. }
  151. }
  152. $this->checkImgExists($pagefile, $img, $src, $value);
  153. }
  154. /**
  155. * Attempt to re-create images that don't exist, when possible
  156. *
  157. * @param Pagefile $pagefile
  158. * @param $img
  159. * @param $src
  160. * @param $value
  161. *
  162. */
  163. protected function checkImgExists(Pagefile $pagefile, $img, $src, &$value) {
  164. $basename = basename($src);
  165. $pathname = $pagefile->pagefiles->path() . $basename;
  166. if(file_exists($pathname)) return; // no action necessary
  167. // file referenced in <img> tag does not exist, and it is not a variation we can re-create
  168. if($pagefile->basename == $basename) {
  169. // original file no longer exists
  170. $this->error("Original image file on {$this->page->path} no longer exists, unable to create new variation ($basename)");
  171. if($this->page->of()) $value = str_replace($img, '', $value); // remove reference to image, when output formatting is on
  172. return;
  173. }
  174. // check if this is a variation that we might be able to re-create
  175. $info = $pagefile->isVariation($basename);
  176. if(!$info) {
  177. // file is not a variation, so we apparently have no source to pull info from
  178. $this->error("Unrecognized image that does not exist ($basename)");
  179. if($this->page->of()) $value = str_replace($img, '', $value); // remove reference to image, when output formatting is on
  180. return;
  181. }
  182. $info['targetName'] = $basename;
  183. $variations = array($info);
  184. while(!empty($info['parent'])) {
  185. $variations[] = $info['parent'];
  186. $info = $info['parent'];
  187. }
  188. foreach(array_reverse($variations) as $info) {
  189. // definitely a variation, attempt to re-create it
  190. $options = array();
  191. if($info['crop']) $options['cropping'] = $info['crop'];
  192. if($info['suffix']) {
  193. $options['suffix'] = $info['suffix'];
  194. if(in_array('hidpi', $options['suffix'])) $options['hidpi'] = true;
  195. }
  196. $newPagefile = $pagefile->size($info['width'], $info['height'], $options);
  197. // $this->wire('log')->message("size($info[width], $info[height], " . print_r($options, true) . ")");
  198. if($newPagefile && is_file($newPagefile->filename())) {
  199. if(!empty($info['targetName']) && $newPagefile->basename != $info['targetName']) {
  200. // new name differs from what is in text. Rename file to be consistent with text.
  201. rename($newPagefile->filename(), $pathname);
  202. }
  203. $this->wire('log')->message("Re-created image variation: $newPagefile->name");
  204. $pagefile = $newPagefile; // for next iteration
  205. } else {
  206. $this->wire('log')->error("Unable to re-create image variation ($newPagefile->name)");
  207. }
  208. }
  209. }
  210. /**
  211. * Record error message to image-errors log
  212. *
  213. * @param string $text
  214. * @param int $flags
  215. * @return this
  216. *
  217. */
  218. public function error($text, $flags = 0) {
  219. $this->wire('log')->save(self::errorLogName, $text);
  220. if($this->wire('modules')->isInstalled('SystemNotifications')) {
  221. $user = $this->wire('modules')->get('SystemNotifications')->getSystemUser();
  222. if($user && !$user->notifications->getBy('title', $text)) {
  223. $no = $user->notifications()->getNew('error');
  224. $no->title = $text;
  225. $no->html = "<p>Field: {$this->field->name}\n<br />Page: <a href='{$this->page->url}'>{$this->page->title}</a></p>";
  226. $user->notifications->save();
  227. }
  228. }
  229. return $this;
  230. }
  231. }