user level

Intermediate

Required products

Flex Builder (Download trial)

Adobe AIR

Sample files

linuxadmin_source.zip

 
Additional Requirements

 
AMFPHP
 
PHP
If you're a Linux administrator, you know all too well the tedium of logging on to your remote server with Secure Shell (SSH), changing directories a dozen times, then finally opening and editing a configuration file with vi or nano. If only there were some sleek, cross-platform tool for easily editing these files! Well, there is. The Adobe AIR desktop development platform, coupled with PHP through AMFPHP, creates a full-featured, triple-operating system toolbox for all your administration needs.
 
The ability to use PHP methods from Adobe Flash opens a whole universe of functionality and a whole universe of security concerns. Do not try this in a production environment without locking it down completely. You can make AMFPHP secure, but that's outside the scope of this article.
 

 
Installing AMFPHP

Your application will be using AMFPHP to communicate with your server. AMFPHP is an open source bridge between the Adobe Action Message Format (AMF) and the PHP programming language. Here's how it works, in a nutshell:
 
  • AMFPHP exposes regular PHP methods to Adobe Flash Player (or Adobe AIR) as remote services.
  • Using ActionScript and MXML (an XML-based markup language) in Adobe Flex Builder, you define the services and how to access them.
  • When the services are called in your application, Flash Player serializes them into the AMF binary format and sends them to AMFPHP.
  • AMFPHP receives the AMF data and converts it to the corresponding data type in PHP. Then, the remote method is executed.
  • If there is a return value for the PHP function, AMFPHP serializes the value into AMF, and then sends it to Flash Player or the AIR wrapper.
To download and install AMFPHP, go to http://www.amfphp.org, and then click the Download link. A list of available packages appears on SourceForge. This article and the accompanying project use the AMFPHP 1.9 release from 20 January 2008.
 
After you've saved the AMFPHP archive, you must extract the contents into your server's web root. The result should be all the AMFPHP files beneath the webroot/amfphp/ directory. To check the installation, go to http://YourServer/amfphp/gateway.php. You should see a page similar to Figure 1.
 
Figure 1. Installing AMFPHP
Figure 1. Installing AMFPHP
 
If you don't see a confirmation page, double-check your file structure and permissions. If you still need help, go to AMFPHP's installation page. There, you can find documentation and video tutorials on AMFPHP.
 
Now that AMFPHP is installed and functioning, you must create services for AMFPHP to expose to your application. AMFPHP can use virtually any PHP method, but you must do a bit of preparation to make sure it can recognize your services. Begin by creating a new PHP file and naming it configService.php. On the second line, right after your opening <php> tag, type:
 
class configService { function configService() {
It is very important that your class declaration and constructor match your filename here. If they don't all match, AMFPHP won't know how to handle the PHP file. Next, declare a methodTable. The methodTable is like a table of contents for AMFPHP, letting it know which functions are available and whether they're accessible remotely. To establish your first method, type:
 
$this->methodTable = array( "openConfig" => array( "description" => "Returns a configuration file as a string", "access" => "remote" ) ); }
The above code tells AMFPHP that you're going to have a method called openConfig and that it will be accessible remotely. It also closes your constructor.
 
Next, you must actually write the openConfig function:
 
function openConfig($filename) { $handle = fopen($filename, "r"); $contents = fread($handle, filesize($filename)); fclose($handle); return $contents; }
The openConfig function accepts one argument: $filename. It then opens the file located at $filename, reads its contents, and returns the contents as a string.
 
Now that you have a methodTable and a corresponding function, you're ready to publish the service. To do that, save your configService.php file in your webroot/amfphp/services directory. To make sure that AMFPHP can access the file and method within, go to http://YourServer/amfphp/browser/. You should see configService listed in the left pane of the AMFPHP services browser. Click it, and you should see your openConfig function as an available method in the right pane. To test it, type the absolute path of a configuration file on your server, and then click Call. The file's contents should appear in the lower portion of the window, as Figure 2 shows.
 
Figure 2. The configuration file's contents in the AMFPHP services browser
Figure 2. The configuration file's contents in the AMFPHP services browser 
 
 
To complete your PHP for the demo application, place a comma (,) after the "access" => "remote" closing parenthesis, and then add this entry:
 
"writeConfig" => array( "description" => "Returns the list of content items", "access" => "remote" ),
And below your openConfig function, add this function:
 
function writeConfig($file) { $filecontents = $file[1]; $fullpath = $file[0]; $handle = fopen($file[0],'r+'); $updatedfile = fwrite($handle, $filecontents); fclose($handle); return 'Updated'; }
Now, you have the necessary PHP methods to open a configuration file and save it after it has changed.
 

 
Creating the FlexMin Configuration File Editor in Flex Builder

The first step in creating your application is to create a new project in Flex Builder. From the menu, choose File > New > Flex Project. In the resulting window, name your project (FlexMin in the accompanying sample application), and choose Desktop Application as the project type. Click Next, accept the defaults, and then click Finish. Flex Builder now sets up your new project in the workspace. When it's finished, you should see your main application file loaded in the editor pane.
 
The first snippet of code you'll add provides an ApplicationControlBar, or menu bar, and some controls to allow users to enter necessary information:
 
<mx:ApplicationControlBar dock="true"> <mx:Label text="Server Address:" fontSize="12" color="#FFFFFF"/> <!-- these 2 lines will be for entering the server IP or hostname --> <mx:TextInput id="serverInput"/> <mx:Label text="Config File:" fontSize="12" color="#FFFFFF"/> <!-- these 2 lines will be for entering the path to the config file or choosing a popular one from a drop-down --> <mx:ComboBox id="configCB" dataProvider="[PHP, Apache, Custom]" color="#FEFEFE" change="checkCustom()"> </mx:ComboBox> <mx:Button label="Load" color="#FAFBFB" click="getConfig()" id="button1"/> <!-- and a button to submit --> </mx:ApplicationControlBar>
Next, create a RemoteObject to access the services that AMFPHP exposed:
 
<mx:RemoteObject id="roConfigService" endpoint="http://192.168.1.104/amfphp" destination="AMFPHP1_9" source="configService" showBusyCursor="true" fault="Alert.show( event.fault.faultDetail , 'error' )" makeObjectsBindable="true"> <mx:method name="openConfig" result="loadConfig()" /> <mx:method name="writeConfig" result="writeResult()" /> </mx:RemoteObject>
The mx:RemoteObject contains several crucial pieces of information:
 
  • endpoint: This is the URL for your AMFPHP installation. Because you're going to allow the user to specify a server in the application, this URL won't actually be used, but it should be defined to avoid compiler errors.
  • fault: If there is a problem with your service, you'd like to see an alert with the error message.
  • source: This property must match with the filename and class name in your AMFPHP service file (configService, in this case).
  • method: Use <mx:method> tags to define the available methods in your configService.php file. Once again, use the same name here and in your configService.php file.
Next, add an <mx:Script> block, or ActionScript section, to your code. Begin it by declaring some variables:
 
<mx:Script> <![CDATA[ import mx.controls.Alert; [Bindable] private var phpConfigPath:String = '/etc/php.ini'; [Bindable] private var apacheConfigPath:String = '/etc/httpd/conf/httpd.conf'; [Bindable] private var startPos:int = 0; [Bindable] private var currentFile:String = '';
The variables phpConfigPath and apacheConfigPath point to the locations of those configuration files in a CentOS 5 (and, presumably, Red Hat) default setup. These will be used to locate the "shortcuts" you created in the MXML combo box earlier. The startPos variable will be used later for a search function, and the currentFile variable will help you keep track of the file you're editing.
 
Continue in the script block with the following code:
 
private function getConfig():void { roConfigService.endpoint = 'http://' + serverInput.text + '/amfphp/gateway.php'; if (configCB.selectedLabel == 'PHP') { roConfigService.openConfig.send(phpConfigPath); currentFile = phpConfigPath; } if (configCB.selectedLabel == 'Custom') { roConfigService.openConfig.send(pathInput.text); currentFile = pathInput.text; } if (configCB.selectedLabel == 'Apache') { roConfigService.openConfig.send(apacheConfigPath); currentFile = apacheConfigPath; } }
This function sets the endpoint for your remote service based on the user's input, and then sends a call to your openConfig method with the selected file path as an argument. When the method finishes, you need a function to handle the result. The function below is named in your RemoteObject as the result handler. Add it to the script block:
 
private function loadConfig():void { currentState = 'configLoaded'; fileTA.text = roConfigService.openConfig.lastResult; }
Flex view states are used to add, remove, and modify components on the stage. The above function changes the application's state and populates a text area with the string that the remote method returns. For this to work properly, you must add a new view state (which includes the text area and a search widget) outside the script block:
 
<mx:states> <mx:State name="configLoaded"> <mx:AddChild position="lastChild"> <mx:TextArea x="195" y="10" width="580" height="410" id="fileTA" backgroundAlpha="0.85" wordWrap="false" editable="true"/> </mx:AddChild> <mx:AddChild position="lastChild"> <mx:TextInput x="10" y="30" id="searchTB"/> </mx:AddChild> <mx:AddChild position="lastChild"> <mx:Label x="10" y="11" text="Find:" fontSize="12" color="#FBFDFD"/> </mx:AddChild> <mx:AddChild position="lastChild"> <mx:Button x="10" y="60" label="Go" click="findText()"/> </mx:AddChild> <mx:AddChild position="lastChild"> <mx:Button x="58" y="60" label="Next" click="findNext()"/> </mx:AddChild> <mx:AddChild position="lastChild"> <mx:Button x="118" y="60" label="Prev" click="findPrevious()"/> </mx:AddChild> <mx:SetStyle target="{configCB}" name="color" value="#FDFEFE"/> <mx:SetEventHandler target="{configCB}" name="change" handler="checkCustom()"/> <mx:AddChild position="lastChild"> <mx:Button x="251" y="428" label="Save Changes" click="writeFile()"/> </mx:AddChild> <mx:AddChild position="lastChild"> <mx:Button x="379" y="428" label="Restart Apache" click="roConfigService.restartApache.send()"/> </mx:AddChild> </mx:State>
And before you close your <mx:states> tag, you need to add a state that will allow the user to enter a custom config file path:
 
<mx:State name="enterCustom"> <mx:AddChild relativeTo="{button1}" position="before"> <mx:Label text="Path:" color="#FCFEFE" fontSize="12"/> </mx:AddChild> <mx:AddChild relativeTo="{button1}" position="before"> <mx:TextInput id="pathInput" toolTip="Enter an absolute path here, like /etc/httpd.conf"/> </mx:AddChild> </mx:State> </mx:states>
At this point, the FlexMin application can request a config file from the server, accept the result, and display it in a text area. However, the user must be able to enter a custom config file path by switching the application's state to enterCustom, declared above. Use this function inside your script block:
 
private function checkCustom():void { if (configCB.selectedLabel == 'Custom') { currentState = 'enterCustom'; } else { currentState = ''; } }
This function is called when the user selects a config file option from the configCB combo box. If Custom is selected, the user is given a text input control to enter the path. Otherwise, one of the predefined paths is used.
 
Next, add functionality to the filtering widget you added in the configLoaded view state. The three main functions of a text search tool are:
 
  • Locate an instance of the specified text.
  • Locate the next instance of the specified text.
  • Locate the previous instance of the specified text.
Therefore, you need three similar functions. I cover the first in more detail and highlight the differences in the other two. Add these three functions to your script block:
 
private function findText():void { var str:String = fileTA.text; // First we'll load the contents of our config file var matchText:String = searchTB.text; // Then we'll get the user's input from the search box startPos = str.search(matchText); // We need to get the position of the search match to highlight it in the text area var selLength:int = startPos + matchText.length; // To highlight the whole matched term, add the term's length to the position of the match fileTA.setSelection(startPos, selLength); // Actually select the text using the selLength calculation fileTA.setFocus(); // focus on the text area to show the highlighted term } private function findNext():void { var str:String = fileTA.text; var matchText:String = searchTB.text; var newPos:int = str.indexOf(matchText,startPos + matchText.length); // We need to set the search start position AFTER the first match so it doesn't find it again var selLength:int = newPos + matchText.length; fileTA.setSelection(newPos, selLength); fileTA.setFocus(); startPos = newPos; // Save the position so that this function can run over and over without finding previous matches } private function findPrevious():void { var str:String = fileTA.text; var matchText:String = searchTB.text; var newPos:int = str.lastIndexOf(matchText,startPos - matchText.length); // When you use lastIndexOf, the search goes right-to-left, AKA backwards var selLength:int = newPos + matchText.length; fileTA.setSelection(newPos, selLength); fileTA.setFocus(); startPos = newPos; // Save the position so that this function can run over and over without finding previous matches }
Now that you can search your config files, editing them will be a breeze. But when you're finished, you must save the changes. You already have the PHP method on the server, and the writeConfig method is declared in the MXML RemoteObject. The only thing missing is a little ActionScript code to call the method and handle the result. Add this to the script block:
 
private function writeFile():void { var file:Array = new Array(); file[0] = currentFile; file[1] = fileTA.text; roConfigService.writeConfig.send(file); } private function writeResult():void { Alert.show(roConfigService.writeConfig.lastResult, 'Result'); }
When you click Save, the file and its path are sent to AMFPHP, where PHP saves the file on the server. AMFPHP returns the text "Updated" on a successful save and a fault message if the save operation is unsuccessful. Bear in mind that your file's permissions must be set to allow writing from the PHP user.
 
Now, you're ready to run your application. Save the file, click the Run icon, and your application should launch. Type the IP address or hostname of your server, choose a config file, and click Load. The result should look like Figure 3.
 
Figure 3. Running the application
Figure 3. Running the application
 
Now that you've got opening, editing, and saving files under your belt, you might want to delve a bit deeper with AMFPHP and AIR. Because you can call virtually any PHP method from AIR, you can even add commands to restart the services that your modifications affect to apply the changes immediately. For example, if you use the FlexMin application to edit your php.ini file, you might want to issue an exec(httpd -k restart) command to see your changes in action. By leveraging the capabilities of PHP through a sleek and friendly AIR interface, you can make your Linux administration and housekeeping tasks a lot less painful.