Bladeren bron

New module to easily manage attachments

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1417 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 14 jaren geleden
bovenliggende
commit
ae54731d16

+ 108 - 0
modules/itop-attachments/ajax.attachment.php

@@ -0,0 +1,108 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+/**
+ * Handles various ajax requests
+ *
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
+ */
+
+require_once('../../approot.inc.php');
+require_once(APPROOT.'/application/application.inc.php');
+require_once(APPROOT.'/application/webpage.class.inc.php');
+require_once(APPROOT.'/application/ajaxwebpage.class.inc.php');
+
+try
+{
+	require_once(APPROOT.'/application/startup.inc.php');
+//	require_once(APPROOT.'/application/user.preferences.class.inc.php');
+	
+	require_once(APPROOT.'/application/loginwebpage.class.inc.php');
+	LoginWebPage::DoLogin(false /* bMustBeAdmin */, true /* IsAllowedToPortalUsers */); // Check user rights and prompt if needed
+	
+	$oPage = new ajax_page("");
+	$oPage->no_cache();
+	
+	$sOperation = utils::ReadParam('operation', '');
+
+	switch($sOperation)
+	{
+	case 'add':
+		$aResult = array(
+			'error' => '',
+			'att_id' => 0,
+			'msg' => ''
+		);
+		$sObjClass = stripslashes(utils::ReadParam('obj_class', ''));
+		$sTempId = utils::ReadParam('temp_id', '');
+		if (empty($sObjClass))
+		{
+			$aResult['error'] = "Missing argument 'obj_class'";
+		}
+		elseif (empty($sTempId))
+		{
+			$aResult['error'] = "Missing argument 'temp_id'";
+		}
+		else
+		{
+			try
+			{
+		   		$oDoc = utils::ReadPostedDocument('file');
+		   		$oAttachment = MetaModel::NewObject('Attachment');
+		   		$oAttachment->Set('expire', time() + 3600); // one hour...
+		   		$oAttachment->Set('temp_id', $sTempId);
+		   		$oAttachment->Set('item_class', $sObjClass);
+		   		$oAttachment->Set('item_id', 0);
+		   		$oAttachment->Set('contents', $oDoc);
+		   		$iAttId = $oAttachment->DBInsert();
+		   		
+		   		$aResult['msg'] = $oDoc->GetFileName();
+		   		$aResult['icon'] = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($oDoc->GetFileName());
+		   		$aResult['att_id'] = $iAttId;
+			}
+			catch (FileUploadException $e)
+			{
+					$aResult['error'] = $e->GetMessage();
+			}
+	   }
+	   $oPage->add(json_encode($aResult));
+		break;
+	
+	case 'remove':
+	$iAttachmentId = utils::ReadParam('att_id', '');
+	$oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE id = :id");
+	$oSet = new DBObjectSet($oSearch, array(), array('id' => $iAttachmentId));
+	while ($oAttachment = $oSet->Fetch())
+	{
+		$oAttachment->DBDelete();
+	}
+	break;
+
+	default:
+		$oPage->p("Missing argument 'operation'");
+	}
+
+	$oPage->output();
+}
+catch (Exception $e)
+{
+	echo $e->GetMessage();
+	IssueLog::Error($e->getMessage());
+}
+?>

+ 191 - 0
modules/itop-attachments/ajaxfileupload.js

@@ -0,0 +1,191 @@
+
+jQuery.extend(
+{
+    createUploadIframe: function(id, uri)
+	{
+			//create frame
+            var frameId = 'jUploadFrame' + id;
+            var iframeHtml = '<iframe id="' + frameId + '" name="' + frameId + '" style="position:absolute; top:-9999px; left:-9999px"';
+			if(window.ActiveXObject)
+			{
+                if(typeof uri== 'boolean')
+                {
+					iframeHtml += ' src="' + 'javascript:false' + '"';
+                }
+                else if(typeof uri== 'string')
+                {
+					iframeHtml += ' src="' + uri + '"';
+                }	
+			}
+			iframeHtml += ' />';
+			jQuery(iframeHtml).appendTo(document.body);
+
+            return jQuery('#' + frameId).get(0);			
+    },
+    createUploadForm: function(id, fileElementId)
+	{
+		//create form	
+		var formId = 'jUploadForm' + id;
+		var fileId = 'jUploadFile' + id;
+		var form = jQuery('<form  action="" method="POST" name="' + formId + '" id="' + formId + '" enctype="multipart/form-data"></form>');	
+		var oldElement = jQuery('#' + fileElementId);
+		var newElement = jQuery(oldElement).clone();
+		jQuery(oldElement).attr('id', fileId);
+		jQuery(oldElement).before(newElement);
+		jQuery(oldElement).appendTo(form);
+		//set attributes
+		jQuery(form).css('position', 'absolute');
+		jQuery(form).css('top', '-1200px');
+		jQuery(form).css('left', '-1200px');
+		jQuery(form).appendTo('body');		
+		return form;
+    },
+
+    ajaxFileUpload: function(s) {
+        // TODO introduce global settings, allowing the client to modify them for all requests, not only timeout		
+        s = jQuery.extend({}, jQuery.ajaxSettings, s);
+        var id = new Date().getTime();        
+		var form = jQuery.createUploadForm(id, s.fileElementId);
+		var io = jQuery.createUploadIframe(id, s.secureuri);
+		var frameId = 'jUploadFrame' + id;
+		var formId = 'jUploadForm' + id;		
+        // Watch for a new set of requests
+        if ( s.global && ! jQuery.active++ )
+		{
+			jQuery.event.trigger( "ajaxStart" );
+		}            
+        var requestDone = false;
+        // Create the request object
+        var xml = {};   
+        if ( s.global )
+            jQuery.event.trigger("ajaxSend", [xml, s]);
+        // Wait for a response to come back
+        var uploadCallback = function(isTimeout)
+		{			
+			var io = document.getElementById(frameId);
+            try 
+			{				
+				if(io.contentWindow)
+				{
+					 xml.responseText = io.contentWindow.document.body?io.contentWindow.document.body.innerHTML:null;
+                	 xml.responseXML = io.contentWindow.document.XMLDocument?io.contentWindow.document.XMLDocument:io.contentWindow.document;
+					 
+				}else if(io.contentDocument)
+				{
+					 xml.responseText = io.contentDocument.document.body?io.contentDocument.document.body.innerHTML:null;
+                	xml.responseXML = io.contentDocument.document.XMLDocument?io.contentDocument.document.XMLDocument:io.contentDocument.document;
+				}						
+            }
+            catch(e)
+			{
+				jQuery.handleError(s, xml, null, e);
+			}
+            if ( xml || isTimeout == "timeout") 
+			{				
+                requestDone = true;
+                var status;
+                try {
+                    status = isTimeout != "timeout" ? "success" : "error";
+                    // Make sure that the request was successful or notmodified
+                    if ( status != "error" )
+					{
+                        // process the data (runs the xml through httpData regardless of callback)
+                        var data = jQuery.uploadHttpData( xml, s.dataType );    
+                        // If a local callback was specified, fire it and pass it the data
+                        if ( s.success )
+                            s.success( data, status );
+    
+                        // Fire the global callback
+                        if( s.global )
+                            jQuery.event.trigger( "ajaxSuccess", [xml, s] );
+                    } else
+                        jQuery.handleError(s, xml, status);
+                } catch(e) 
+				{
+                    status = "error";
+                    jQuery.handleError(s, xml, status, e);
+                }
+
+                // The request was completed
+                if( s.global )
+                    jQuery.event.trigger( "ajaxComplete", [xml, s] );
+
+                // Handle the global AJAX counter
+                if ( s.global && ! --jQuery.active )
+                    jQuery.event.trigger( "ajaxStop" );
+
+                // Process result
+                if ( s.complete )
+                    s.complete(xml, status);
+
+                jQuery(io).unbind();
+
+                setTimeout(function()
+									{	try 
+										{
+											jQuery(io).remove();
+											jQuery(form).remove();	
+											
+										} catch(e) 
+										{
+											jQuery.handleError(s, xml, null, e);
+										}									
+
+									}, 100)
+
+                xml = null
+
+            }
+        }
+        // Timeout checker
+        if ( s.timeout > 0 ) 
+		{
+            setTimeout(function(){
+                // Check to see if the request is still happening
+                if( !requestDone ) uploadCallback( "timeout" );
+            }, s.timeout);
+        }
+        try 
+		{
+
+			var form = jQuery('#' + formId);
+			jQuery(form).attr('action', s.url);
+			jQuery(form).attr('method', 'POST');
+			jQuery(form).attr('target', frameId);
+            if(form.encoding)
+			{
+				jQuery(form).attr('encoding', 'multipart/form-data');      			
+            }
+            else
+			{	
+				jQuery(form).attr('enctype', 'multipart/form-data');			
+            }			
+            jQuery(form).submit();
+
+        } catch(e) 
+		{			
+            jQuery.handleError(s, xml, null, e);
+        }
+		
+		jQuery('#' + frameId).load(uploadCallback	);
+        return {abort: function () {}};	
+
+    },
+
+    uploadHttpData: function( r, type ) {
+        var data = !type;
+        data = type == "xml" || data ? r.responseXML : r.responseText;
+        // If the type is "script", eval it in global context
+        if ( type == "script" )
+            jQuery.globalEval( data );
+        // Get the JavaScript object, if JSON is used.
+        if ( type == "json" )
+            eval( "data = " + data );
+        // evaluate scripts within html
+        if ( type == "html" )
+            jQuery("<div>").html(data).evalScripts();
+
+        return data;
+    }
+})
+

+ 39 - 0
modules/itop-attachments/en.dict.attachments.php

@@ -0,0 +1,39 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+/**
+ * Localized data
+ *
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
+ */
+
+Dict::Add('EN US', 'English', 'English', array(
+	'Attachments:TabTitle_Count' => 'Attachments (%1$d)',
+	'Attachments:EmptyTabTitle' => 'Attachments',
+	'Attachments:FieldsetTitle' => 'Attachments',
+	'Attachments:DeleteBtn' => 'Delete',
+	'Attachments:History_File_Added' => 'Attachment %1$s added.',
+	'Attachments:History_File_Removed' => 'Attachment %1$s removed.',
+	'Attachments:AddAttachment' => 'Add attachment: ',
+	'Attachments:UploadNotAllowedOnThisSystem' => 'File upload in NOT allowed on this system.',
+	'Attachment:Max_Go' => '(Maximum file size: %1$s Go)',
+	'Attachment:Max_Mo' => '(Maximum file size: %1$s Mo)',
+	'Attachment:Max_Ko' => '(Maximum file size: %1$s Ko)',
+));
+?>

+ 39 - 0
modules/itop-attachments/fr.dict.attachments.php

@@ -0,0 +1,39 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+/**
+ * Localized data
+ *
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
+ */
+
+Dict::Add('FR FR', 'French', 'Français', array(
+	'Attachments:TabTitle_Count' => 'Pièces jointes (%1$d)',
+	'Attachments:EmptyTabTitle' => 'Pièces jointes',
+	'Attachments:FieldsetTitle' => 'Pièces jointes',
+	'Attachments:DeleteBtn' => 'Supprimer',
+	'Attachments:History_File_Added' => 'Ajout de la pièce jointe: %1$s.',
+	'Attachments:History_File_Removed' => 'Suppression de la pièce jointe: %1$s.',
+	'Attachments:AddAttachment' => 'Ajouter une pièce jointe: ',
+	'Attachments:UploadNotAllowedOnThisSystem' => 'Le téléchargement de fichiers est interdit sur ce système.',
+	'Attachment:Max_Go' => '(Taille de fichier max.: %1$s Gb)',
+	'Attachment:Max_Mo' => '(Taille de fichier max.: %1$s Mb)',
+	'Attachment:Max_Ko' => '(Taille de fichier max.: %1$s Kb)',
+));
+?>

BIN
modules/itop-attachments/icons/doc.png


BIN
modules/itop-attachments/icons/document.png


BIN
modules/itop-attachments/icons/html.png


BIN
modules/itop-attachments/icons/image.png


BIN
modules/itop-attachments/icons/odp.png


BIN
modules/itop-attachments/icons/ods.png


BIN
modules/itop-attachments/icons/odt.png


BIN
modules/itop-attachments/icons/pdf.png


BIN
modules/itop-attachments/icons/ppt.png


BIN
modules/itop-attachments/icons/rtf.png


BIN
modules/itop-attachments/icons/txt.png


BIN
modules/itop-attachments/icons/xls.png


BIN
modules/itop-attachments/icons/zip.png


+ 540 - 0
modules/itop-attachments/model.attachments.php

@@ -0,0 +1,540 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+/**
+ * Module attachments
+ * 
+ * A quick and easy way to upload and attach files to *any* (see Configuration below) object in the CMBD in one click
+ *
+ * Configuration: the list of classes for which the "Attachments" tab is visible is defined via the module's 'allowed_classes'
+ * configuration parameter. By default the tab is active for all kind of Tickets.
+ *
+ * @author      Erwan Taloc <erwan.taloc@combodo.com>
+ * @author      Romain Quetiez <romain.quetiez@combodo.com>
+ * @author      Denis Flaven <denis.flaven@combodo.com>
+ * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
+ */
+
+class Attachment extends DBObject
+{
+	public static function Init()
+	{
+		$aParams = array
+		(
+			"category" => "addon",
+			"key_type" => "autoincrement",
+			"name_attcode" => array('item_class', 'temp_id'),
+			"state_attcode" => "",
+			"reconc_keys" => array(),
+			"db_table" => "attachment",
+			"db_key_field" => "id",
+			"db_finalclass_field" => "",
+		);
+		MetaModel::Init_Params($aParams);
+		MetaModel::Init_InheritAttributes();
+
+		MetaModel::Init_AddAttribute(new AttributeDateTime("expire", array("allowed_values"=>null, "sql"=>"expire", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeString("temp_id", array("allowed_values"=>null, "sql"=>"temp_id", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
+
+		MetaModel::Init_AddAttribute(new AttributeString("item_class", array("allowed_values"=>null, "sql"=>"item_class", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
+		MetaModel::Init_AddAttribute(new AttributeString("item_id", array("allowed_values"=>null, "sql"=>"item_id", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
+
+		MetaModel::Init_AddAttribute(new AttributeBlob("contents", array("depends_on"=>array())));
+
+		MetaModel::Init_SetZListItems('details', array('temp_id', 'item_class', 'item_id'));
+		MetaModel::Init_SetZListItems('advanced_search', array('temp_id', 'item_class', 'item_id'));
+		MetaModel::Init_SetZListItems('standard_search', array('temp_id', 'item_class', 'item_id'));
+		MetaModel::Init_SetZListItems('list', array('temp_id', 'item_class', 'item_id'));
+	}
+
+	// Todo - implement a cleanup function (see a way to do that generic !)
+}
+
+class AttachmentPlugIn implements iApplicationUIExtension, iApplicationObjectExtension
+{
+	public function OnDisplayProperties($oObject, WebPage $oPage, $bEditMode = false)
+	{
+	}
+
+	public function OnDisplayRelations($oObject, WebPage $oPage, $bEditMode = false)
+	{
+		$bDisplayTab = $this->IsTargetObject($oObject);
+		// Exit here if the class is not allowed
+		if (!$bDisplayTab) return;
+		
+		$oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id");
+		$oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey()));
+		$sTitle = ($oSet->Count() > 0)? Dict::Format('Attachments:TabTitle_Count', $oSet->Count()) : Dict::S('Attachments:EmptyTabTitle');
+		$oPage->SetCurrentTab($sTitle);
+		$oPage->add_style(
+<<<EOF
+.attachment {
+	display: inline-block;
+	text-align:center;
+	float:left;
+	padding:5px;	
+}
+.attachment:hover {
+	background-color: #e0e0e0;
+}
+.attachment img {
+	border: 0;
+}
+.attachment a {
+	text-decoration: none;
+	color: #1C94C4;
+}
+.btn_hidden {
+	display: none;
+}
+EOF
+		);
+		$oPage->add('<fieldset>');
+		$oPage->add('<legend>'.Dict::S('Attachments:FieldsetTitle').'</legend>');
+
+		if ($bEditMode)
+		{
+			$sIsDeleteEnabled = $this->m_bDeleteEnabled ? 'true' : 'false';
+			$iTransactionId = $oPage->GetTransactionId();
+			$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'modules/itop-attachments/ajaxfileupload.js');
+			$sClass = get_class($oObject);
+			$sTempId = session_id().'_'.$iTransactionId;
+			$sDeleteBtn = Dict::S('Attachments:DeleteBtn');
+			$oPage->add_script(
+<<<EOF
+	function RemoveNewAttachment(att_id)
+	{
+		$('#attachment_'+att_id).attr('name', 'removed_attachments[]');
+		$('#display_attachment_'+att_id).hide();
+		return false; // Do not submit the form !
+	}
+	function ajaxFileUpload()
+	{
+		//starting setting some animation when the ajax starts and completes
+		$("#loading").ajaxStart(function(){
+			$(this).show();
+		}).ajaxComplete(function(){
+			$(this).hide();
+		});
+		
+		/*
+			prepareing ajax file upload
+			url: the url of script file handling the uploaded files
+                        fileElementId: the file type of input element id and it will be the index of  $_FILES Array()
+			dataType: it support json, xml
+			secureuri:use secure protocol
+			success: call back function when the ajax complete
+			error: callback function when the ajax failed
+			
+                */
+		$.ajaxFileUpload
+		(
+			{
+				url: GetAbsoluteUrlAppRoot()+'modules/itop-attachments/ajax.attachment.php?obj_class={$sClass}&temp_id={$sTempId}&operation=add', 
+				secureuri:false,
+				fileElementId:'file',
+				dataType: 'json',
+				success: function (data, status)
+				{
+					if(typeof(data.error) != 'undefined')
+					{
+						if(data.error != '')
+						{
+							alert(data.error);
+						}
+						else
+						{
+							var sDownloadLink = GetAbsoluteUrlAppRoot()+'pages/ajax.render.php/?operation=download_document&class=Attachment&id='+data.att_id+'&field=contents';
+							$('#attachments').append('<div class="attachment" id="display_attachment_'+data.att_id+'"><a href="'+sDownloadLink+'"><img src="'+data.icon+'"><br/>'+data.msg+'<input id="attachment_'+data.att_id+'" type="hidden" name="attachments[]" value="'+data.att_id+'"/></a><br/><input type="button" class="btn_hidden" value="{$sDeleteBtn}" onClick="RemoveNewAttachment('+data.att_id+');"/></div>');
+							if($sIsDeleteEnabled)
+							{
+								$('#display_attachment_'+data.att_id).hover( function() { $(this).children(':button').toggleClass('btn_hidden'); } );
+							}
+							//alert(data.msg);
+						}
+					}
+				},
+				error: function (data, status, e)
+				{
+					alert(e);
+				}
+			}
+		)
+		
+		return false;
+
+	}
+EOF
+);
+			$oPage->add('<span id="attachments">');
+			while ($oAttachment = $oSet->Fetch())
+			{
+				$iAttId = $oAttachment->GetKey();
+				$oDoc = $oAttachment->Get('contents');
+				$sFileName = $oDoc->GetFileName();
+				$sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName);
+				$sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php/?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents';
+				$oPage->add('<div class="attachment" id="attachment_'.$iAttId.'"><a href="'.$sDownloadLink.'"><img src="'.$sIcon.'"><br/>'.$sFileName.'<input type="hidden" name="attachments[]" value="'.$iAttId.'"/></a><br/>&nbsp;<input id="btn_remove_'.$iAttId.'" type="button" class="btn_hidden" value="Delete" onClick="$(\'#attachment_'.$iAttId.'\').remove();"/>&nbsp;</div>');
+			}
+			$oPage->add('</span>');			
+			$oPage->add('<div style="clear:both"></div>');			
+			$sMaxUpload = $this->GetMaxUpload();
+			$oPage->p(Dict::S('Attachments:AddAttachment').'<input type="file" name="file" id="file" onChange="ajaxFileUpload();"><span style="display:none;" id="loading">&nbsp;<img src="../images/indicator.gif"></span> '.$sMaxUpload);
+			//$oPage->p('<input type="button" onClick="ajaxFileUpload();" value=" Upload !">');
+			$oPage->p('<span style="display:none;" id="loading">Loading, please wait...</span>');
+			$oPage->add('</fieldset>');
+			if ($this->m_bDeleteEnabled)
+			{
+				$oPage->add_ready_script('$(".attachment").hover( function() {$(this).children(":button").toggleClass("btn_hidden"); } );');
+			}
+		}
+		else
+		{
+			$oPage->add('<span id="attachments">');
+			while ($oAttachment = $oSet->Fetch())
+			{
+				$iAttId = $oAttachment->GetKey();
+				$oDoc = $oAttachment->Get('contents');
+				$sFileName = $oDoc->GetFileName();
+				$sIcon = utils::GetAbsoluteUrlAppRoot().AttachmentPlugIn::GetFileIcon($sFileName);
+				$sDownloadLink = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php/?operation=download_document&class=Attachment&id='.$iAttId.'&field=contents';
+				$oPage->add('<div class="attachment" id="attachment_'.$iAttId.'"><a href="'.$sDownloadLink.'"><img src="'.$sIcon.'"><br/>'.$sFileName.'</a><input type="hidden" name="attachments[]" value="'.$iAttId.'"/><br/>&nbsp;&nbsp;</div>');
+			}
+		}
+	}
+
+	public function OnFormSubmit($oObject, $sFormPrefix = '')
+	{
+		if ($this->IsTargetObject($oObject))
+		{
+			// For new objects attachments are processed in OnDBInsert
+			if (!$oObject->IsNew())
+			{
+				self::UpdateAttachments($oObject);
+			}
+		}
+	}
+
+	protected function GetMaxUpload()
+	{
+		$iMaxUpload = ini_get('upload_max_filesize');
+		if (!$iMaxUpload)
+		{
+			$sRet = Dict::S('Attachments:UploadNotAllowedOnThisSystem');
+		}
+		else
+		{
+			$iMaxUpload = utils::ConvertToBytes($iMaxUpload);
+			if ($iMaxUpload > 1024*1024*1024)
+			{
+				$sRet = Dict::Format('Attachment:Max_Go', sprintf('%0.2f', $iMaxUpload/(1024*1024*1024)));
+			}
+			else if ($iMaxUpload > 1024*1024)
+			{
+				$sRet = Dict::Format('Attachment:Max_Mo', sprintf('%0.2f', $iMaxUpload/(1024*1024)));
+			}
+			else
+			{
+				$sRet = Dict::Format('Attachment:Max_Ko', sprintf('%0.2f', $iMaxUpload/(1024)));
+			}
+		}
+		return $sRet;
+	}
+	
+	public function OnFormCancel($sTempId)
+	{
+		// Delete all "pending" attachments for this form
+		$sOQL = 'SELECT Attachment WHERE temp_id = :temp_id';
+		$oSearch = DBObjectSearch::FromOQL($sOQL);
+		$oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId));
+		while($oAttachment = $oSet->Fetch())
+		{
+			$oAttachment->DBDelete();
+			// Pending attachment, don't mention it in the history
+		}
+	}
+
+	public function EnumUsedAttributes($oObject)
+	{
+		return array();
+	}
+
+	public function GetIcon($oObject)
+	{
+		return '';
+	}
+
+	public function GetHilightClass($oObject)
+	{
+		// Possible return values are:
+		// HILIGHT_CLASS_CRITICAL, HILIGHT_CLASS_WARNING, HILIGHT_CLASS_OK, HILIGHT_CLASS_NONE	
+		return HILIGHT_CLASS_NONE;
+	}
+
+	public function EnumAllowedActions(DBObjectSet $oSet)
+	{
+		// No action
+		return array();
+    }
+
+	public function OnIsModified($oObject)
+	{
+		if ($this->IsTargetObject($oObject))
+		{
+			$aAttachmentIds = utils::ReadParam('attachments', array());
+			$aRemovedAttachmentIds = utils::ReadParam('removed_attachments', array());
+			if ( (count($aAttachmentIds) > 0) || (count($aRemovedAttachmentIds) > 0) )
+			{
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public function OnCheckToWrite($oObject)
+	{
+		return array();
+	}
+
+	public function OnCheckToDelete($oObject)
+	{
+		return array();
+	}
+
+	public function OnDBUpdate($oObject, $oChange = null)
+	{
+	}
+	
+	public function OnDBInsert($oObject, $oChange = null)
+	{
+		if ($this->IsTargetObject($oObject))
+		{
+			self::UpdateAttachments($oObject, $oChange);
+		}
+	}
+	
+	public function OnDBDelete($oObject, $oChange = null)
+	{
+		if ($this->IsTargetObject($oObject))
+		{
+			$oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id");
+			$oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey()));
+			while ($oAttachment = $oSet->Fetch())
+			{
+				$oAttachment->DBDelete();
+			}
+		}			
+	}
+
+	///////////////////////////////////////////////////////////////////////////////////////////////////////
+	//
+	// Plug-ins specific functions
+	//
+	///////////////////////////////////////////////////////////////////////////////////////////////////////
+	
+	protected function IsTargetObject($oObject)
+	{
+		$aAllowedClasses = MetaModel::GetModuleSetting('attachments', 'allowed_classes', array('Ticket'));
+		foreach($aAllowedClasses as $sAllowedClass)
+		{
+			if ($oObject instanceof $sAllowedClass)
+			{
+				return true;
+			}
+		}
+		return false;
+	}
+
+	var $m_bDeleteEnabled = true;
+
+	public function EnableDelete($bEnabled)
+	{
+		$this->m_bDeleteEnabled = $bEnabled;
+	}
+
+
+	protected static function UpdateAttachments($oObject, $oChange = null)
+	{
+		$iTransactionId = utils::ReadParam('transaction_id', null);
+		if (!is_null($iTransactionId))
+		{
+			$aActions = array();
+			$aAttachmentIds = utils::ReadParam('attachments', array());
+
+			// Get all current attachments
+			$oSearch = DBObjectSearch::FromOQL("SELECT Attachment WHERE item_class = :class AND item_id = :item_id");
+			$oSet = new DBObjectSet($oSearch, array(), array('class' => get_class($oObject), 'item_id' => $oObject->GetKey()));
+			while ($oAttachment = $oSet->Fetch())
+			{
+				// Remove attachments that are no longer attached to the current object
+				if (!in_array($oAttachment->GetKey(), $aAttachmentIds))
+				{
+					$oAttachment->DBDelete();
+					$aActions[] = self::GetActionDescription($oAttachment, false /* false => deletion */);
+				}
+			}			
+
+			// Attach new (temporary) attachements
+			$sTempId = session_id().'_'.$iTransactionId;
+			// The object is being created from a form, check if there are pending attachments
+			// for this object, but deleting the "new" ones that were already removed from the form
+			$aRemovedAttachmentIds = utils::ReadParam('removed_attachments', array());
+			$sOQL = 'SELECT Attachment WHERE temp_id = :temp_id';
+			$oSearch = DBObjectSearch::FromOQL($sOQL);
+			foreach($aAttachmentIds as $iAttachmentId)
+			{
+				$oSet = new DBObjectSet($oSearch, array(), array('temp_id' => $sTempId));
+				while($oAttachment = $oSet->Fetch())
+				{
+					if (in_array($oAttachment->GetKey(),$aRemovedAttachmentIds))
+					{
+						$oAttachment->DBDelete();
+						// temporary attachment removed, don't even mention it in the history
+					}
+					else
+					{
+						$oAttachment->Set('item_id', $oObject->GetKey());
+						$oAttachment->Set('temp_id', '');
+						$oAttachment->DBUpdate();
+						// temporary attachment confirmed, list it in the history
+						$aActions[] = self::GetActionDescription($oAttachment, true /* true => creation */);
+					}
+				}
+			}
+			if (count($aActions) > 0)
+			{
+				if ($oChange == null)
+				{
+					// Let's create a change if non is supplied
+					$oChange = MetaModel::NewObject("CMDBChange");
+					$oChange->Set("date", time());
+					$sUserString = CMDBChange::GetCurrentUserName();
+					$oChange->Set("userinfo", $sUserString);
+					$iChangeId = $oChange->DBInsert();							
+				}
+				foreach($aActions as $sActionDescription)
+				{
+					self::RecordHistory($oChange, $oObject, $sActionDescription);
+				}
+			}
+		}
+	}
+	
+	/////////////////////////////////////////////////////////////////////////////////////////
+	public static function GetFileIcon($sFileName)
+	{
+		$aPathParts = pathinfo($sFileName);
+		switch($aPathParts['extension'])
+		{
+			case 'doc':
+			case 'docx':
+			$sIcon = 'doc.png';
+			break;
+			
+			case 'xls':
+			case 'xlsx':
+			$sIcon = 'xls.png';
+			break;
+			
+			case 'ppt':
+			case 'pptx':
+			$sIcon = 'ppt.png';
+			break;
+			
+			case 'pdf':
+			$sIcon = 'pdf.png';
+			break;
+			
+			case 'txt':
+			case 'text':
+			$sIcon = 'txt.png';
+			break;
+			
+			case 'rtf':
+			$sIcon = 'rtf.png';
+			break;
+			
+			case 'odt':
+			$sIcon = 'odt.png';
+			break;
+			
+			case 'ods':
+			$sIcon = 'ods.png';
+			break;
+			
+			case 'odp':
+			$sIcon = 'odp.png';
+			break;
+			
+			case 'html':
+			case 'htm':
+			$sIcon = 'html.png';
+			break;
+			
+			case 'png':
+			case 'gif':
+			case 'jpg':
+			case 'jpeg':
+			case 'tiff':
+			case 'tif':
+			case 'bmp':
+			$sIcon = 'image.png';
+			
+			break;
+			case 'zip':
+			case 'gz':
+			case 'tgz':
+			case 'rar':
+			$sIcon = 'zip.png';
+			break;
+
+			default:
+			$sIcon = 'document.png';
+			break;
+		}
+		
+		return "modules/itop-attachments/icons/$sIcon";
+	}
+	
+	/////////////////////////////////////////////////////////////////////////
+	private static function RecordHistory(CMDBChange $oChange, $oTargetObject, $sDescription)
+	{
+		$oMyChangeOp = MetaModel::NewObject("CMDBChangeOpPlugin");
+		$oMyChangeOp->Set("change", $oChange->GetKey());
+		$oMyChangeOp->Set("objclass", get_class($oTargetObject));
+		$oMyChangeOp->Set("objkey", $oTargetObject->GetKey());
+		$oMyChangeOp->Set("description", $sDescription);
+		$iId = $oMyChangeOp->DBInsertNoReload();
+	}
+
+	/////////////////////////////////////////////////////////////////////////
+	private static function GetActionDescription($oAttachment, $bCreate = true)
+	{
+		$oBlob = $oAttachment->Get('contents');
+		$sFileName = $oBlob->GetFileName();
+		if ($bCreate)
+		{
+			$sDescription = Dict::Format('Attachments:History_File_Added', $sFileName);
+		}
+		else
+		{
+			$sDescription = Dict::Format('Attachments:History_File_Removed', $sFileName);
+		}
+		return $sDescription;
+	}	
+}
+
+?>

+ 65 - 0
modules/itop-attachments/module.attachments.php

@@ -0,0 +1,65 @@
+<?php
+// Copyright (C) 2010 Combodo SARL
+//
+//   This program is free software; you can redistribute it and/or modify
+//   it under the terms of the GNU General Public License as published by
+//   the Free Software Foundation; version 3 of the License.
+//
+//   This program is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU General Public License for more details.
+//
+//   You should have received a copy of the GNU General Public License
+//   along with this program; if not, write to the Free Software
+//   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+SetupWebPage::AddModule(
+	__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
+	'itop-attachments/1.0.0',
+	array(
+		// Identification
+		//
+		'label' => 'Tickets attachments',
+		'category' => 'business',
+
+		// Setup
+		//
+		'dependencies' => array(
+			
+		),
+		'mandatory' => false,
+		'visible' => true,
+
+		// Components
+		//
+		'datamodel' => array(
+			'model.attachments.php'
+		),
+		'webservice' => array(
+			
+		),
+		'dictionary' => array(
+			'en.dict.attachments.php',
+			'fr.dict.attachments.php',
+		),
+		'data.struct' => array(
+			// add your 'structure' definition XML files here,
+		),
+		'data.sample' => array(
+			// add your sample data XML files here,
+		),
+		
+		// Documentation
+		//
+		'doc.manual_setup' => '', // hyperlink to manual setup documentation, if any
+		'doc.more_information' => '', // hyperlink to more information, if any 
+
+		// Default settings
+		//
+		'settings' => array(
+			'allowed_classes' => array('Ticket'), // List of classes for which the "Attachments" tab is visible
+		),
+	)
+);
+?>