inlineimage.class.inc.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. <?php
  2. // Copyright (C) 2016 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. define('INLINEIMAGE_DOWNLOAD_URL', 'pages/ajax.render.php?operation=download_document&class=InlineImage&field=contents&id=');
  19. /**
  20. * Persistent classes (internal): store images referenced inside HTML formatted text fields
  21. *
  22. * @copyright Copyright (C) 2016 Combodo SARL
  23. * @license http://opensource.org/licenses/AGPL-3.0
  24. */
  25. class InlineImage extends DBObject
  26. {
  27. public static function Init()
  28. {
  29. $aParams = array
  30. (
  31. 'category' => 'addon',
  32. 'key_type' => 'autoincrement',
  33. 'name_attcode' => array('item_class', 'temp_id'),
  34. 'state_attcode' => '',
  35. 'reconc_keys' => array(''),
  36. 'db_table' => 'inline_image',
  37. 'db_key_field' => 'id',
  38. 'db_finalclass_field' => '',
  39. 'indexes' => array(
  40. array('temp_id'),
  41. array('item_class', 'item_id'),
  42. array('item_org_id'),
  43. ),
  44. );
  45. MetaModel::Init_Params($aParams);
  46. MetaModel::Init_InheritAttributes();
  47. MetaModel::Init_AddAttribute(new AttributeDateTime("expire", array("allowed_values"=>null, "sql"=>'expire', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false)));
  48. MetaModel::Init_AddAttribute(new AttributeString("temp_id", array("allowed_values"=>null, "sql"=>'temp_id', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false)));
  49. MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values"=>null, "sql"=>'item_class', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false)));
  50. MetaModel::Init_AddAttribute(new AttributeObjectKey("item_id", array("class_attcode"=>'item_class', "allowed_values"=>null, "sql"=>'item_id', "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false)));
  51. MetaModel::Init_AddAttribute(new AttributeInteger("item_org_id", array("allowed_values"=>null, "sql"=>'item_org_id', "default_value"=>'0', "is_null_allowed"=>true, "depends_on"=>array(), "always_load_in_tables"=>false)));
  52. MetaModel::Init_AddAttribute(new AttributeBlob("contents", array("is_null_allowed"=>false, "depends_on"=>array(), "always_load_in_tables"=>false)));
  53. MetaModel::Init_SetZListItems('details', array('temp_id', 'item_class', 'item_id', 'item_org_id'));
  54. MetaModel::Init_SetZListItems('standard_search', array('temp_id', 'item_class', 'item_id'));
  55. MetaModel::Init_SetZListItems('list', array('temp_id', 'item_class', 'item_id' ));
  56. }
  57. /**
  58. * Maps the given context parameter name to the appropriate filter/search code for this class
  59. * @param string $sContextParam Name of the context parameter, e.g. 'org_id'
  60. * @return string Filter code, e.g. 'customer_id'
  61. */
  62. public static function MapContextParam($sContextParam)
  63. {
  64. if ($sContextParam == 'org_id')
  65. {
  66. return 'item_org_id';
  67. }
  68. else
  69. {
  70. return null;
  71. }
  72. }
  73. /**
  74. * Set/Update all of the '_item' fields
  75. * @param DBObject $oItem Container item
  76. * @return void
  77. */
  78. public function SetItem(DBObject $oItem, $bUpdateOnChange = false)
  79. {
  80. $sClass = get_class($oItem);
  81. $iItemId = $oItem->GetKey();
  82. $this->Set('item_class', $sClass);
  83. $this->Set('item_id', $iItemId);
  84. $aCallSpec = array($sClass, 'MapContextParam');
  85. if (is_callable($aCallSpec))
  86. {
  87. $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter
  88. if (MetaModel::IsValidAttCode($sClass, $sAttCode))
  89. {
  90. $iOrgId = $oItem->Get($sAttCode);
  91. if ($iOrgId > 0)
  92. {
  93. if ($iOrgId != $this->Get('item_org_id'))
  94. {
  95. $this->Set('item_org_id', $iOrgId);
  96. if ($bUpdateOnChange)
  97. {
  98. $this->DBUpdate();
  99. }
  100. }
  101. }
  102. }
  103. }
  104. }
  105. /**
  106. * Give a default value for item_org_id (if relevant...)
  107. * @return void
  108. */
  109. public function SetDefaultOrgId()
  110. {
  111. // First check that the organization CAN be fetched from the target class
  112. //
  113. $sClass = $this->Get('item_class');
  114. $aCallSpec = array($sClass, 'MapContextParam');
  115. if (is_callable($aCallSpec))
  116. {
  117. $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter
  118. if (MetaModel::IsValidAttCode($sClass, $sAttCode))
  119. {
  120. // Second: check that the organization CAN be fetched from the current user
  121. //
  122. if (MetaModel::IsValidClass('Person'))
  123. {
  124. $aCallSpec = array($sClass, 'MapContextParam');
  125. if (is_callable($aCallSpec))
  126. {
  127. $sAttCode = call_user_func($aCallSpec, 'org_id'); // Returns null when there is no mapping for this parameter
  128. if (MetaModel::IsValidAttCode($sClass, $sAttCode))
  129. {
  130. // OK - try it
  131. //
  132. $oCurrentPerson = MetaModel::GetObject('Person', UserRights::GetContactId(), false);
  133. if ($oCurrentPerson)
  134. {
  135. $this->Set('item_org_id', $oCurrentPerson->Get($sAttCode));
  136. }
  137. }
  138. }
  139. }
  140. }
  141. }
  142. }
  143. /**
  144. * When posting a form, finalize the creation of the inline images
  145. * related to the specified object
  146. *
  147. * @param DBObject $oObject
  148. */
  149. public static function FinalizeInlineImages(DBObject $oObject)
  150. {
  151. $iTransactionId = utils::ReadParam('transaction_id', null);
  152. if (!is_null($iTransactionId))
  153. {
  154. // Attach new (temporary) inline images
  155. $sTempId = session_id().'_'.$iTransactionId;
  156. // The object is being created from a form, check if there are pending inline images for this object
  157. $sOQL = 'SELECT InlineImage WHERE temp_id = :temp_id';
  158. $oSearch = DBObjectSearch::FromOQL($sOQL);
  159. $oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId));
  160. while($oInlineImage = $oSet->Fetch())
  161. {
  162. $oInlineImage->SetItem($oObject);
  163. $oInlineImage->Set('temp_id', '');
  164. $oInlineImage->DBUpdate();
  165. }
  166. }
  167. }
  168. /**
  169. * Cleanup the pending images if the form is not submitted
  170. * @param string $sTempId
  171. */
  172. public static function OnFormCancel($sTempId)
  173. {
  174. // Delete all "pending" InlineImages for this form
  175. $sOQL = 'SELECT InlineImage WHERE temp_id = :temp_id';
  176. $oSearch = DBObjectSearch::FromOQL($sOQL);
  177. $oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId));
  178. while($oInlineImage = $oSet->Fetch())
  179. {
  180. $oInlineImage->DBDelete();
  181. }
  182. }
  183. /**
  184. * Parses the supplied HTML fragment to rebuild the attribute src="" for images
  185. * that refer to an InlineImage (detected via the attribute data-img-id="") so that
  186. * the URL is consistent with the current URL of the application.
  187. * @param string $sHtml The HTML fragment to process
  188. * @return string The modified HTML
  189. */
  190. public static function FixUrls($sHtml)
  191. {
  192. $aNeedles = array();
  193. $aReplacements = array();
  194. // Find img tags with an attribute data-img-id
  195. if (preg_match_all('/<img ([^>]*)data-img-id="([0-9]+)"([^>]*)>/i', $sHtml, $aMatches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE))
  196. {
  197. $sUrl = utils::GetAbsoluteUrlAppRoot().INLINEIMAGE_DOWNLOAD_URL;
  198. foreach($aMatches as $aImgInfo)
  199. {
  200. $sImgTag = $aImgInfo[0][0];
  201. $sAttId = $aImgInfo[2][0];
  202. $sNewImgTag = preg_replace('/src="[^"]+"/', 'src="'.$sUrl.$sAttId.'"', $sImgTag); // preserve other attributes
  203. $aNeedles[] = $sImgTag;
  204. $aReplacements[] = $sNewImgTag;
  205. }
  206. $sHtml = str_replace($aNeedles, $aReplacements, $sHtml);
  207. }
  208. return $sHtml;
  209. }
  210. /**
  211. * Get the javascript fragment - to be added to "on document ready" - to adjust (on the fly) the width on Inline Images
  212. */
  213. public static function FixImagesWidth()
  214. {
  215. $iMaxWidth = (int)MetaModel::GetConfig()->Get('inline_image_max_display_width', 0);
  216. $sJS = '';
  217. if ($iMaxWidth != 0)
  218. {
  219. $sJS =
  220. <<<EOF
  221. $('img[data-img-id]').each(function() {
  222. if ($(this).width() > $iMaxWidth)
  223. {
  224. $(this).css({'max-width': '{$iMaxWidth}px', width: '', height: '', 'max-height': ''});
  225. }
  226. $(this).addClass('inline-image').attr('href', $(this).attr('src'));
  227. }).magnificPopup({type: 'image', closeOnContentClick: true });
  228. EOF
  229. ;
  230. }
  231. return $sJS;
  232. }
  233. /**
  234. * Check if an the given mimeType is an image that can be processed by the system
  235. * @param string $sMimeType
  236. * @return boolean
  237. */
  238. public static function IsImage($sMimeType)
  239. {
  240. if (!function_exists('gd_info')) return false; // no image processing capability on this system
  241. $bRet = false;
  242. $aInfo = gd_info(); // What are the capabilities
  243. switch($sMimeType)
  244. {
  245. case 'image/gif':
  246. return $aInfo['GIF Read Support'];
  247. break;
  248. case 'image/jpeg':
  249. return $aInfo['JPEG Support'];
  250. break;
  251. case 'image/png':
  252. return $aInfo['PNG Support'];
  253. break;
  254. }
  255. return $bRet;
  256. }
  257. /**
  258. * Resize an image so that it fits the maximum width/height defined in the config file
  259. * @param ormDocument $oImage The original image stored as an array (content / mimetype / filename)
  260. * @return ormDocument The resampled image (or the original one if it already fit)
  261. */
  262. public static function ResizeImageToFit(ormDocument $oImage, &$aDimensions = null)
  263. {
  264. $img = false;
  265. switch($oImage->GetMimeType())
  266. {
  267. case 'image/gif':
  268. case 'image/jpeg':
  269. case 'image/png':
  270. $img = @imagecreatefromstring($oImage->GetData());
  271. break;
  272. default:
  273. // Unsupported image type, return the image as-is
  274. $aDimensions = null;
  275. return $oImage;
  276. }
  277. if ($img === false)
  278. {
  279. $aDimensions = null;
  280. return $oImage;
  281. }
  282. else
  283. {
  284. // Let's scale the image, preserving the transparency for GIFs and PNGs
  285. $iWidth = imagesx($img);
  286. $iHeight = imagesy($img);
  287. $aDimensions = array('width' => $iWidth, 'height' => $iHeight);
  288. $iMaxImageSize = (int)MetaModel::GetConfig()->Get('inline_image_max_storage_width', 0);
  289. if (($iMaxImageSize > 0) && ($iWidth <= $iMaxImageSize) && ($iHeight <= $iMaxImageSize))
  290. {
  291. // No need to resize
  292. return $oImage;
  293. }
  294. $fScale = min($iMaxImageSize / $iWidth, $iMaxImageSize / $iHeight);
  295. $iNewWidth = $iWidth * $fScale;
  296. $iNewHeight = $iHeight * $fScale;
  297. $aDimensions['width'] = $iNewWidth;
  298. $aDimensions['height'] = $iNewHeight;
  299. $new = imagecreatetruecolor($iNewWidth, $iNewHeight);
  300. // Preserve transparency
  301. if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png"))
  302. {
  303. imagecolortransparent($new, imagecolorallocatealpha($new, 0, 0, 0, 127));
  304. imagealphablending($new, false);
  305. imagesavealpha($new, true);
  306. }
  307. imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight);
  308. ob_start();
  309. switch ($oImage->GetMimeType())
  310. {
  311. case 'image/gif':
  312. imagegif($new); // send image to output buffer
  313. break;
  314. case 'image/jpeg':
  315. imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality
  316. break;
  317. case 'image/png':
  318. imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression
  319. break;
  320. }
  321. $oNewImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName());
  322. @ob_end_clean();
  323. imagedestroy($img);
  324. imagedestroy($new);
  325. return $oNewImage;
  326. }
  327. }
  328. /**
  329. * Get the (localized) textual representation of the max upload size
  330. * @return string
  331. */
  332. public static function GetMaxUpload()
  333. {
  334. $iMaxUpload = ini_get('upload_max_filesize');
  335. if (!$iMaxUpload)
  336. {
  337. $sRet = Dict::S('Attachments:UploadNotAllowedOnThisSystem');
  338. }
  339. else
  340. {
  341. $iMaxUpload = utils::ConvertToBytes($iMaxUpload);
  342. if ($iMaxUpload > 1024*1024*1024)
  343. {
  344. $sRet = Dict::Format('Attachment:Max_Go', sprintf('%0.2f', $iMaxUpload/(1024*1024*1024)));
  345. }
  346. else if ($iMaxUpload > 1024*1024)
  347. {
  348. $sRet = Dict::Format('Attachment:Max_Mo', sprintf('%0.2f', $iMaxUpload/(1024*1024)));
  349. }
  350. else
  351. {
  352. $sRet = Dict::Format('Attachment:Max_Ko', sprintf('%0.2f', $iMaxUpload/(1024)));
  353. }
  354. }
  355. return $sRet;
  356. }
  357. /**
  358. * Get the fragment of javascript needed to complete the initialization of
  359. * CKEditor when creating/modifying an object
  360. *
  361. * @param DBObject $oObject The object being edited
  362. * @param string $sTempId The concatenation of session_id().'_'.$iTransactionId.
  363. * @return string The JS fragment to insert in "on document ready"
  364. */
  365. public static function EnableCKEditorImageUpload(DBObject $oObject, $sTempId)
  366. {
  367. $sObjClass = get_class($oObject);
  368. $iObjKey = $oObject->GetKey();
  369. return
  370. <<<EOF
  371. // Hook the file upload of all CKEditor instances
  372. $('.htmlEditor').each(function() {
  373. var oEditor = $(this).ckeditorGet();
  374. oEditor.config.extraPlugins = 'uploadimage';
  375. oEditor.config.uploadUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php';
  376. oEditor.config.filebrowserBrowseUrl = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php?operation=cke_browse&temp_id=$sTempId&obj_class=$sObjClass&obj_key=$iObjKey';
  377. oEditor.on( 'fileUploadResponse', function( evt ) {
  378. var fileLoader = evt.data.fileLoader;
  379. var xhr = fileLoader.xhr;
  380. var data = evt.data;
  381. try {
  382. var response = JSON.parse( xhr.responseText );
  383. // Error message does not need to mean that upload finished unsuccessfully.
  384. // It could mean that ex. file name was changes during upload due to naming collision.
  385. if ( response.error && response.error.message ) {
  386. data.message = response.error.message;
  387. }
  388. // But !uploaded means error.
  389. if ( !response.uploaded ) {
  390. evt.cancel();
  391. } else {
  392. data.fileName = response.fileName;
  393. data.url = response.url;
  394. // Do not call the default listener.
  395. evt.stop();
  396. }
  397. } catch ( err ) {
  398. // Response parsing error.
  399. data.message = fileLoader.lang.filetools.responseError;
  400. window.console && window.console.log( xhr.responseText );
  401. evt.cancel();
  402. }
  403. } );
  404. oEditor.on( 'fileUploadRequest', function( evt ) {
  405. evt.data.fileLoader.uploadUrl += '?operation=cke_img_upload&temp_id=$sTempId&obj_class=$sObjClass';
  406. }, null, null, 4 ); // Listener with priority 4 will be executed before priority 5.
  407. oEditor.on( 'instanceReady', function() {
  408. oEditor.widgets.registered.uploadimage.onUploaded = function( upload ) {
  409. var oData = JSON.parse(upload.xhr.responseText);
  410. this.replaceWith( '<img src="' + upload.url + '" ' +
  411. 'width="' + oData.width + '" ' +
  412. 'height="' + oData.height + '">' );
  413. }
  414. });
  415. });
  416. EOF
  417. ;
  418. }
  419. }