image_styles_admin.inc 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <?php
  2. /**
  3. * @file Include file for image_styles_admin routines that do not need to be
  4. * loaded on each request.
  5. */
  6. /**
  7. * Menu callback: Duplicates an image style and redirects to the image styles
  8. * overview page.
  9. *
  10. * @param array $style
  11. * An image style array.
  12. *
  13. * @see image_style_name_validate()
  14. */
  15. function image_styles_admin_duplicate_page_callback($style) {
  16. $duplicate_style = image_styles_admin_duplicate($style);
  17. drupal_set_message(t('Style %name has been duplicated to %new_name.', array(
  18. '%name' => isset($style['label']) ? $style['label'] : $style['name'],
  19. '%new_name' => isset($duplicate_style['label']) ? $duplicate_style['label'] : $duplicate_style['name'])));
  20. drupal_goto('admin/config/media/image-styles');
  21. }
  22. /**
  23. * Duplicates an image style and saves it.
  24. *
  25. * @param array $style
  26. * An image style array.
  27. * @param string|null $new_style_name
  28. * The preferred name for the new style. If left empty, the new name will be
  29. * based on the name of the style to duplicate. In both cases and when
  30. * necessary, the new name will be made unique by adding some suffix to it.
  31. * @param string|null $new_style_label
  32. * The preferred label for the new style. If left empty, the new label will be
  33. * based on the label of the style to duplicate. If that one is also empty,
  34. * no label will be defined for the new style, so Drupal (>=7.23) will create
  35. * one.
  36. *
  37. * @return array
  38. * An image style array with the newly created copy of the given style.
  39. *
  40. * @see image_style_name_validate()
  41. */
  42. function image_styles_admin_duplicate($style, $new_style_name = NULL, $new_style_label = NULL) {
  43. // Find a unique name for the copy.
  44. // Step 1: Find the base: name without things like '-copy' or '-copy-1'
  45. $style_name_base = empty($new_style_name) ? $style['name'] : $new_style_name;
  46. if (preg_match('/-copy(-\d+)?$/', $style_name_base)) {
  47. $style_name_base = substr($style_name_base, 0, strpos($style_name_base, '-copy'));
  48. }
  49. // Step 2: Add -copy to it (if the name comes from the current style).
  50. if (empty($new_style_name)) {
  51. $style_name_base .= '-copy';
  52. }
  53. // Step 3: Ensure the new name will be unique.
  54. $i = 0;
  55. $style_name = $style_name_base;
  56. $styles = image_styles();
  57. while (isset($styles[$style_name])) {
  58. $i++;
  59. $style_name = $style_name_base . '-' . $i;
  60. }
  61. $style['name'] = $style_name;
  62. // Step 4: Find a new label for the copy.
  63. if (isset($new_style_label) || isset($style['label'])) {
  64. $style_label = empty($new_style_label) ? $style['label'] : $new_style_label;
  65. $copy = t('copy');
  66. if (preg_match("/ $copy( \d+)?$/", $style_label)) {
  67. $style_label = substr($style_label, 0, strpos($style_label, " $copy"));
  68. }
  69. // Step 4a: Add " copy" to it (if the name comes from the current style).
  70. if (empty($new_style_label)) {
  71. $style_label .= " $copy";
  72. }
  73. // Step 4b: Make "unique" (based on the number added to the name)
  74. if ($i > 0) {
  75. $style['label'] .= " $i";
  76. }
  77. }
  78. // Unset isid to save it as a new style.
  79. unset($style['isid']);
  80. $style = image_style_save($style);
  81. // Save copies of each effect with the new image style ID (isid).
  82. foreach ($style['effects'] as &$effect) {
  83. // Unset ieid to save it as a new effect.
  84. unset($effect['ieid']);
  85. $effect['isid'] = $style['isid'];
  86. $effect = image_effect_save($effect);
  87. }
  88. return $style;
  89. }
  90. /**
  91. * drupal_get_form callback: form to export an image style.
  92. *
  93. * @param array $form
  94. * @param array $form_state
  95. * @param array $style
  96. * An image style array.
  97. *
  98. * @return array
  99. */
  100. function image_styles_admin_export_form($form, $form_state, $style) {
  101. drupal_set_title(format_string('%page_name @style_name',
  102. array('%page_name' => t('Export image style'), '@style_name' => isset($style['label']) ? $style['label'] : $style['name'])),
  103. PASS_THROUGH);
  104. $form['serialized_style'] = array(
  105. '#type' => 'textarea',
  106. '#rows' => 5,
  107. '#title' => t('Image style export data'),
  108. '#default_value' => image_styles_admin_export_serialize($style),
  109. '#attributes' => array('readonly' =>'readonly'),
  110. '#description' => t('Copy the contents of this field to the clipboard and, on another site, paste it in the textarea of an %page_title page.',
  111. array('%page_title' => t('Import image style'))),
  112. );
  113. return $form;
  114. }
  115. /**
  116. * drupal_get_form callback: form to import an image style.
  117. */
  118. function image_styles_admin_import_form($form/*, $form_state*/) {
  119. $form['serialized_style'] = array(
  120. '#type' => 'textarea',
  121. '#rows' => 5,
  122. '#title' => t('Image style import data'),
  123. '#default_value' => '',
  124. '#required' => TRUE,
  125. '#description' => t('Paste the contents of the textarea of an %page_title page into this field.', array('%page_title' => t('Export image style'))),
  126. );
  127. $form['actions'] = array('#type' => 'actions');
  128. $form['actions']['submit'] = array(
  129. '#type' => 'submit',
  130. '#value' => t('Import'),
  131. );
  132. return $form;
  133. }
  134. /**
  135. * Callback to validate the import style form.
  136. */
  137. function image_styles_admin_import_form_validate($form, &$form_state) {
  138. $import = image_styles_admin_unify_newlines($form_state['values']['serialized_style']);
  139. if (image_styles_admin_import_extract_style($import) === FALSE) {
  140. form_set_error('serialized_style', t('The %field cannot be imported as an image style.', array('%field' => t('Image style import data'))));
  141. }
  142. }
  143. /**
  144. * Callback to process form submission of the import style form.
  145. */
  146. function image_styles_admin_import_form_submit($form, &$form_state) {
  147. $import = image_styles_admin_unify_newlines($form_state['values']['serialized_style']);
  148. $style = image_styles_admin_import_extract_style($import);
  149. // Import the style by "duplicating" it, but prevent adding the -copy suffix
  150. // by passing the requested name and label as 2nd and 3rd parameter.
  151. $new_style = image_styles_admin_duplicate($style, $style['name'], isset($style['label']) ? $style['label'] : NULL);
  152. if ($new_style['name'] === $style['name']) {
  153. drupal_set_message(t('Style %name has been imported.', array('%name' => $style['name'])));
  154. }
  155. else {
  156. drupal_set_message(t('Style %name has been imported as %new_name.', array(
  157. '%name' => isset($style['label']) ? $style['label'] : $style['name'],
  158. '%new_name' => isset($new_style['label']) ? $new_style['label'] : $new_style['name'])));
  159. }
  160. drupal_goto('admin/config/media/image-styles');
  161. }
  162. /**
  163. * Serializes image style data so it can be exported.
  164. *
  165. * @param array $style
  166. * An image style array.
  167. *
  168. * @return string
  169. * The serialized image style. Keys that are not needed for import are not
  170. * serialized.
  171. */
  172. function image_styles_admin_export_serialize($style) {
  173. $style = array_intersect_key($style, array('name' => 0, 'label' => 0, 'effects' => 0));
  174. foreach ($style['effects'] as &$effect) {
  175. $effect = array_intersect_key($effect, array('weight' => 0, 'name' => 0, 'data' => 0));
  176. }
  177. array_walk_recursive($style, function(&$value) {
  178. if (is_string($value)) {
  179. $value = image_styles_admin_unify_newlines($value);
  180. }
  181. });
  182. return serialize($style);
  183. }
  184. /**
  185. * Unifies newlines in the string to the Unix newline standard.
  186. *
  187. * #2636314: textareas may convert newlines to the underlying OS style: convert
  188. * all new lines to Unix style before unserializing. As string length is in the
  189. * serialized data, we must ensure that we also do this on each array value
  190. * before serializing.
  191. *
  192. * @param string $str
  193. *
  194. * @return string
  195. */
  196. function image_styles_admin_unify_newlines($str) {
  197. $str = str_replace("\r\n", "\n", $str);
  198. $str = str_replace("\r", "\n", $str);
  199. return $str;
  200. }
  201. /**
  202. * Unserializes and validates a string into image style data.
  203. *
  204. * @param string $import
  205. * The string representation of a @see serialize()'d image style array.
  206. *
  207. * @return array|false
  208. * An image style array or false if the string could not be unserialized into
  209. * image style data.
  210. */
  211. function image_styles_admin_import_extract_style($import) {
  212. $style = unserialize($import);
  213. // Check if the contents of the textarea could be unserialized into an array.
  214. if (!is_array($style)) {
  215. return FALSE;
  216. }
  217. // Filter out keys that we do not process.
  218. $style = array_intersect_key($style, array('name' => 0, 'label' => 0, 'effects' => 0));
  219. // 'name' is required and must be "machine name" string.
  220. if (!isset($style['name']) || !is_string($style['name']) || preg_match('/[0-9a-z_\-]+/', $style['name']) !== 1) {
  221. return FALSE;
  222. }
  223. // Optional 'label' must be a string.
  224. if (isset($style['label']) && !is_string($style['label'])) {
  225. return FALSE;
  226. }
  227. // 'effects' is required and must be an array.
  228. if (!isset($style['effects']) || !is_array($style['effects'])) {
  229. return FALSE;
  230. }
  231. // Check effects elements
  232. foreach ($style['effects'] as &$effect) {
  233. // an effect must be an array.
  234. if (!is_array($effect)) {
  235. return FALSE;
  236. }
  237. // Check if the required keys are available, we will ignore the other.
  238. $effect = array_intersect_key($effect, array('weight' => 0, 'name' => 0, 'data' => 0));
  239. if (count($effect) !== 3) {
  240. return FALSE;
  241. }
  242. // effect weight must be an integer (data type in table is int, not float).
  243. if (!is_int($effect['weight']) && $effect['weight'] !== (string) (int) $effect['weight']) {
  244. return FALSE;
  245. }
  246. // effect name must be a string
  247. if (!is_string($effect['name'])) {
  248. return FALSE;
  249. }
  250. // Check whether the effect data is an array.
  251. if (!is_array($effect['data'])) {
  252. return FALSE;
  253. }
  254. }
  255. // @todo: are there any security implications for creating styles like this?
  256. // - Unserialize() is save in itself: it only creates data (except possibly
  257. // for__wakeup(), but that can only be in already existing code: safe
  258. // - Not expected array entries are removed (array_intersect_key): safe
  259. // - Basic types are checked: safe
  260. // - Effect data array is not checked. Possibly unsafe?! The effect data array
  261. // contains the effect parameters. Normally these are entered and validated
  262. // via a form and subsequently saved in the database (serialized as here).
  263. // The form validation is not executed on import and thus the data may
  264. // contain invalid values. This is acceptable as it can also be done by
  265. // operating directly on the database. In Drupal this is not normally
  266. // checked for during processing: error messages will make clear that the
  267. // data has been played with. Can incorrect data be abused? It may contain:
  268. // - incorrect types: we do not know the data structure of the various
  269. // effects, so we cannot check that and have to accept it as it comes.
  270. // Effects should check_plain in summary theme and convert to int/float
  271. // whenever possible before using it in commands.
  272. // - PHP code, but that may be valid content for the text or custom effects:
  273. // Effects should check_plain in summary theme and convert to int/float
  274. // whenever possible before using it in commands.
  275. // @todo: if the style contains an effect that contains PHP code, the user
  276. // should need the 'use PHP for settings' permission.
  277. // - HTML and or JS code: when used as parameter, this normally won't hurt.
  278. // When showing on the screen (summary theme), proper escaping should
  279. // suffice and is needed anyway: responsibility of effect.
  280. return $style;
  281. }