The user interface (UI) is designed using a modular and (somewhat) object oriented methodology. The general design goal behind the entire UI: any single component can be replaced without needing to rebuild the entire UI.
The UI has four key areas: common code, plugins, plugin structure, and menus structure. Each of these areas cover specific functionality:
The system has been designed with a separation between computations and output. Computations do not need to be rewritten if the output alternates between a web browser, PDF document, XML file, printer, or something else.
The user interface directory structure is as follows:
ui/common/ :: All common code is stored here. ui/html :: This directory contains all static HTML pages ui/images :: This directory contains all image files ui/plugins :: All code for plugins is stored here. ui/template :: The plugin template is stored here.
With the web-based user interface, all URLs are relative to the ui/ directory. Thus, individual developers can have their own ui directories for software development.
Common code is stored in the “ui/common” directory. This code is accessible by everything in the UI: other common functions and plugins.
Common code has no explicit initialization; each common function must validate all dependencies. For example, if a function queries the database then it should make sure the $DB global variable is set:
global $DB;
if (empty($DB)) { return; }
For ease of maintenance, common functions have been grouped together based on feature functionality. For example, common-license.php contains functions related to license information. In this common library are functions such as:
Similarly, common-menu.php contains a library of menu-related functions. In common-license.php are functions like:
Common code is unaware of the output format. Functions that only perform computations (e.g., “find all licenses for a given file” – the LicenseGet() function), do not format the resulting data. Before the data is sent to the output device, it must be formatted by a plugin.
However, some common code functions do generate pre-formatted output. These functions should not perform complex computations. The separation ensures that different output devices only need to call output-specific formatting functions and not output-specific computations. For example, menu_to_1html() generates HTML text, but it does nothing more complicated than traversing the menu data structure.
Common code may have a dependency on plugins. For example, common-license.php accesses the database (via the core-db.php plugin) and common-agents.php accesses plugins that define agents. However, common code must always check to make sure the plugin exists before attempting to access it.
Common code is loaded by the file common/common.php. This PHP file is used to load all defined common files and functions. Beyond this, no initialization is performed. Specifically, common code must not perform any tasks outside of the functions since common code has no means to tell if the functionality is actually required.
There is no specific naming convention used for the common code files or function names. In general, files are named after the general type of task. Function names are descriptive and frequently include the type of task. For example, common-dir.php is used for identifying the directories into which uploaded FOSSology files are stored. Most of the functions begin with “Dir”: Dir2Path() stores an uploaded item's directory as a path, and Dir2Browse() creates the yellow directory box found in the web interface's browse window and search results.
Plugins provide specific input/output features. For example, if you need a UI module for displaying licenses, then that functionality would be defined by a plugin.
Unlike common code, plugins may perform complex computations and format the output within a single function. In fact, different computations may be formed based on the output type.
In addition, while common code is always available, a plugin's availability may vary based on input parameters, global settings, user permissions, and output type. Thus, a plugin may provide a very specific functionality, but may not always be accessible.
For maintenance, plugins are named after their generic category. For example, all of the plugins that begin with “admin-” (e.g., admin-db-vacuum.php and admin-upload-delete.php) perform administrative functions. The “ui-” plugins (e.g., ui-topnav.php and ui-welcome.php) are closely tied to the HTML web-based user interface. And the “jobs-” plugins (e.g., jobs-runningjobs.php and jobs-showjobs-upload.php) handle job-oriented tasks.
While plugins are intended to process input and output, they may not actually generate output that gets displayed. For example, the command-line tool “fossjobs” (cli/fossjobs.php) uses the menu data structure to find all plugins for analysis agents, and calls plugin functions for scheduling analysis. However, the plugins do not actually generate the output for fossjobs.
Since plugins provide feature-specific functionality, adding a new feature only means adding a new plugin (hence the name: plugging in a new feature). Similarly, changing a specific feature only requires altering the specific plugin.
As an example, the web-based user interface for FOSSology has gone through a number of iterations. Some had the menu at the top, others had the menu down the side. Some versions used HTML frames, while the current version does not use frames. All of these changes were limited to very specific plugins:
All plugins are based off of a PHP template called FO_Plugin (ui/template/template-plugin.php). The template defines some public variables that are used for organizing plugins, and some methods common to all plugins.
class FO_Plugin
{
// All public fields can be empty, indicating that it does not apply.
var $State=PLUGIN_STATE_INVALID;
The $State variable identifies whether the plugin is ready, initialized, partially initialized (not yet ready), or unavailable.
var $Name="";
$Name defines the official name of this plugin. The name does not need to be the same as the PHP filename. Other plugins will look for this plugin based on this name.
var $Version="1.0";
The $Version variable permits multiple plugins to have the same name. Only the plugin with the highest version number will be found when searching for a plugin by name (see plugin_find_id() in common/common-plugin.php). The version is very useful for development since you can replace a plugin by having a different file with the same $Name and a newer $Version.
Although plugins can generate any type of output, they are currently used to generate HTML. Thus, there are some variables used to define HTML values and web-based functionality:
var $Title=""; // used for HTML title tags and window menu bars var $NoMenu=0; /* 1 = Don't show the HTML menu at the top of page */ var $NoHeader=0; /* 1 = Don't show the HTML header at the top of page */ var $NoHTML=0; /* 1 = Don't add any HTML to the output */
A plugin's availability may be dependent on some other plugin's availability:
var $PluginLevel=10; /* used for sorting plugins -- higher comes first after dependencies are met */ var $Dependency = array(); /* an array of $Name found in the required plugins */ var $InitOrder=0; /* during initialization, some plugins may need to come first; higher is initialized first */
Dependencies must not be cyclical. If “A” depends on “B” and “B” depends on “A”, then neither will load because the dependency of “must be loaded before” is never met.
Finally, plugins may want to register with the main menu:
var $MenuList=NULL; var $MenuOrder=0; var $MenuTarget=NULL;
For example, to define an “About” menu item under the “Help” menu:
var $MenuList = "Help::About"; var $MenuOrder=0;
And a “delete” agent under the tool, administration menu and displaying in a new window would use:
var $MenuList = "Tools::Administration::Delete"; var $MenuOrder=0; var $MenuTarget="_new"; /* HTML <a target="_new"> */
All plugins are based on a template that defines universal functionality. For example, every plugin has a function called “Initialize()”. The plugin may override the default function for plugin-specific needs, but the function always exists. The default functions are as follows:
The following functions are only called when source code is placed on the system. They are intended to perform any system-specific configurations prior to using the plugin.
The Install() functions are called by the fossinit command-line program. The 'fossinit' program is a simple UI that loads all plugins and runs every Install() function.
Currently, there is no “fossremove” script, so the Remove() function is not used. When it is eventually created, this will remove tables, values, and dependent files that are only used by this plugin.
Each time a UI (web or command-line) loads the plugins, the following functions are accessed:
At this point, the plugin is fully initialized and ready for operation. Calling function may call Action() to perform a default action, OutputOpen()/Output()/OutputClose() to generate output, or OutputSet()/Output()/OutputUnset() to generate output that will be included within other output.
After the UI completes, the instantiated plugins are removed.
Some groups of plugins have additional, standardized functions. Any other plugin with a dependency on these specific subclasses can assume that these standardized functions exist (see Plugin Structure).
For example, every “agent” plugin (plugin for scheduling an analysis task) has two additional functions:
Every agent that registers itself with the “agent” menu structure must have both of these functions defined.
As another example, every “upload-” plugin (plugin used for uploading a file into the FOSSology system) has a function called Upload().
The use of these standardized functions simplifies the implementation in other plugins. For example, a UI plugin for selecting an analysis method does not need to know about every individual “agent” plugin. Instead, it can access the list of plugins that registered as agents and can call a generic method to see if the agent needs to be scheduled for analysis. If a new analysis agent is added to the system, then no other code needs to be modified in order to use the new agent.
For FOSSology, most plugins and common code access the database. Typically, one would think that the widely-used database access code would be a common-code library. However, the actual database connection and query code is located in a plugin (ui/plugins/core-db.php) and not in common code. This is intentional and done for the following reasons:
Because the database is widely used by the entire system, the class pointer for the database plugin is stored in a global variable: $DB. The database plugin is the only plugin referenced directly by a global variable. For example, $DB→Action(“$SQL”);
Each time FOSSology is accessed, the plugins are initialized and register with a global plugin structure (see the Plugin Process Flow diagram). This data structure tracks each plugin, common-name, and its state.
Plugins may have a dependency on one or more other plugins. This dependency is defined in the (template-defined) array “$Dependency”. For example, viewing the license histogram (ui/plugins/ui-license.php with common name “license”) has a dependency on plugins with common names “db”, “browse”, and “view-license”.
class ui_license extends FO_Plugin
{
var $Name = "license";
var $Title = "License Browser";
var $Version = "1.0";
var $Dependency = array("db","browse","view-license");
...
What this means is that the license plugin will not be postinitialized if any of these dependencies are missing. In effect, this feature will not exist unless all dependencies are met.
Plugins may use other plugins that are not explicitly listed as dependencies. This allows plugins to exist and work even if an optional dependency does not exist. However, the plugin must not assume that an optional dependency is initialized before the plugin, itself, initializes. To check for optional dependencies, there is a function in common-plugin.php called plugin_find_id(). If the desired plugin exists, then an id will be returned.
if (plugin_find_id("view-license") >= 0)
{
// plugin_find_id("view-license") is the index into the array of plugins.
$ModLicView = &$Plugins[plugin_find_id("view-license")];
}
A plugin may choose not to initialize based on input and global settings. For example, there is a plugin called “core-init.php”. This provides a feature with the common name “init”. This plugin only initializes when a global flag indicates that the system is being installed and needs to be initialized.
Plugins also have the ability to alter the states of other plugins. For example, the “init” (core-init.php) and “auth” (core-auth.php) plugins both disable other plugins. This way, uninitialized plugins are not accessessible and users who are not authenticated (feature provided by the “auth” plugin) cannot access some other plugins.
The core-init.php plugin is only used when code is installed on the system. This plugin is responsible for ensuring that every other plugin does not run until the database has been properly configured and the Install() function has been called.
The init plugin (ui/plugins/core-init.php) has a non-default PostInitialize() function. This function looks for a semaphore that indicates the need for an initialization. The semaphore, a file named “init.ui” in the UI directory, denotes the semaphore. If the file exists then an initialization is required. If the semaphore does not exist, then this plugin disables itself.
$Filename = getcwd() . "/init.ui";
if (!file_exists($Filename))
{
$this->State = PLUGIN_STATE_INVALID;
return;
}
The second thing init does is disable all other plugins. Since plugins are loaded in dependency order, every plugin already fully-initialized must be a dependency; thus, they are kept running. All other plugins are disabled (state set to 'INVALID') before they can become initialized. Disabling plugins ensures that they will not be initialized before they are installed.
$Max = count($Plugins);
for($i=0; $i < $Max; $i++)
{
$P = &$Plugins[$i];
if ($P->State == PLUGIN_STATE_INVALID) { continue; }
/* Don't turn off plugins that are already up and running. */
if ($P->State == PLUGIN_STATE_READY) { continue; }
if ($P->DBaccess == PLUGIN_DB_DEBUG) { continue; }
$Key = array_search($P->Name,$this->Dependency);
if (($Key === FALSE) && strcmp($P->Name,$this->Name))
{
// print "Disable " . $P->Name . " as $Key <br>\n";
$P->Destroy();
$P->State = PLUGIN_STATE_INVALID;
}
}
Finally, the init plugin adds itself to the menu. This gives web users the menu option “Admin::Initialize”.
if ((@$_SESSION['UserLevel'] >= PLUGIN_DB_USERADMIN) && ($this->MenuList !== ""))
{
menu_insert("Main::" . $this->MenuList,$this->MenuOrder,$this->Name,$this->MenuTarget);
}
The core-init.php plugin works closely with the core-schema.php plugin. Init disables all plugins and schema configures the system.
The init Output() function calls the ApplySchema() public function, found in ui/plugins/core-schema.php. This adds any missing tables, columns, views, etc. removes unused tables/columns/views/etc., inserts any missing default values, and runs every Install() function on every plugin.
From core-init.php:
$Schema = &$Plugins[plugin_find_any_id("schema")];
$FailFlag = $Schema->ApplySchema($Schema->Filename,0,0);
and from core-schema.php:
print " Initializing plugins\n"; flush();
for($i=0; $i < $Max; $i++)
{
$P = &$Plugins[$i];
/* Init ALL plugins */
if ($Debug) { print " Initializing plugin '" . $P->Name . "'\n"; }
$State = $P->Install();
if ($State != 0)
{
$FailFlag = 1;
print "FAILED: " . $P->Name . " failed to install.\n"; flush();
return(1);
}
}
Finally, the init plugin removes the semaphore. This ensures that future connections will not re-attempt to reconfigure the database and run Install() on every plugin.
$State = unlink($Filename);
The auth plugin (ui/plugins/core-auth.php) enforces user-level authentication. Users without the appropriate credentials will not have access to certain plugins.
The Install() function ensures that the database table for 'users' contains at least one default user. Otherwise, auth will block every plugin – including init – from working.
The magic for auth happens in the PostInitialize() function. Login sessions are stored by the web server and access via the $_SESSION[] global hash.
$Max = count($Plugins);
for($i=0; $i < $Max; $i++)
{
$P = &$Plugins[$i];
if ($P->State == PLUGIN_STATE_INVALID) { continue; }
if (($P->DBaccess > $Level) || (empty($_SESSION['User']) && $P->LoginFlag) )
{
$P->Destroy();
$P->State = PLUGIN_STATE_INVALID;
}
}
The auth Output() function has two states:
The menu data structure is used for registering plugins with other plugins. It is called “menu” because most registrations are used to render pull-down or list-based menus.
This structure actually defines a suite of menu structures. Each element in the structure contains a name, sort-order, link to a plugin, HTML-specific code, URI, and a submenu. Each of these items are optional.
The menu structure is ordered by explicit sort priority and name. Each menu entry also defines the path through the menu. The top-most path identifies the core functional area. Everything below it is used for registration.
A few examples:
For example:
Main
Home
Search
Browse
Upload
Instructions
From File
From Server
From URL
One-Shot License
Organize
Folders
Create
Delete Folder
Edit Properties
Move
...
As a more detailed view, the main menu may include the following menu tree summary (found by using “Help::Debug::Debug Menus” which calls the core-debug-menus.php plugin):
1. Home (18 -- Default @ _top)
2. Search (14 -- search_file @ )
3. Browse (12 -- browse @ )
4. Upload (10 -- @ )
4.1. Instructions (10 -- upload_instructions @ )
4.2. From File (0 -- upload_file @ )
4.3. From Server (0 -- upload_srv_files @ )
4.4. From URL (0 -- upload_url @ )
4.5. One-Shot License (0 -- agent_license_once @ )
5. Organize (8 -- @ )
5.1. Folders (0 -- @ )
5.1.1. Create (0 -- folder_create @ )
5.1.2. Delete Folder (0 -- admin_folder_delete @ )
5.1.3. Edit Properties (0 -- folder_properties @ )
5.1.4. Move (0 -- folder_move @ )
5.2. License (0 -- @ )
5.2.1. Default Groups (0 -- license_groups_default @ )
5.2.2. Default Terms (0 -- license_terms_default @ )
5.2.3. Manage Groups (0 -- license_groups_manage @ )
5.2.4. Manage Terms (0 -- licterm_manage @ )
5.3. Uploads (0 -- @ )
5.3.1. Delete Uploaded File (0 -- admin_upload_delete @ )
5.3.2. Edit Properties (0 -- upload_properties @ )
5.3.3. Move (0 -- upload_move @ )
5.3.4. Remove License Analysis (0 -- agent_reset_license @ )
The summary provides a quick overview of the actual menu structure. This lists the menu's name, order number (before the '–') and optional plugin associated with the menu item (after the '–'). If the resulting web page should be associated with an alternate target window, then that window name is after the '@'.
The full menu structure contains much more information. For example, the full “Search” menu item looks like:
menu Object ( [Name] => Search [URI] => search_file [HTML] => [Order] => 14 [Target] => [MaxDepth] => 0 [SubMenu] => [FullName] => Main::Search [Title] => )
The 'FullName' is the path through the menu. The 'Name' is the text that appears in the menu. If HTML had a value, then that value would be used instead of the Name text.
Since this menu item appears under the 'Main' branch, it would normally be sorted alphabetically. This would make the top menu from the web browser: “Admin Browse Help Home Jobs Organize Search Upload”. However, the unsorted ordering is not optimal for usability. The 'Order' is used to override alphabetical ordering. In this case, 'Search' has an order of 14. It will come after all menu items that have values > 14, and before all menu items that have values < 14. If there are other menu items with an order value of 14, then they will be sorted alphabetically.
Finally, the URI is set. Clicking on the 'Search' menu item will activate “${URI}?mod=search_file”. This module is provided by ui/plugins/search-file.php.
For the UI, there is a large amount of error detection, but virtually no error reporting. For detection, every function validates parameters and checks return codes to ensure active calls worked properly.
However, while parameters are validated and calls are checked, very few errors are reported to the user. Errors are only shown to the user if there is something the user can do about it. Since FOSSology is designed for use by many different types of users, and not just administrators with root access, reporting issues such as “the database is down” or “disk full” is only useful if the user can do something about it. Depending on your user level, you may never see these errors.
Instead of reporting cryptic error messages to the user, the FOSSology UI simply “does the right thing”. If the right thing is to not schedule an analysis, then it is not scheduled. If the right thing is to inform the user, then the user is informed. This is very different than some applications and operating systems that constantly alert the user to errors that are beyond the user's control.
The largest risk to any user interface comes from hostile input. The input may be an overflow or SQL injection, or may be an honest cut-and-paste error. From the UI's viewpoint, these are all handled the same way:
$URI = Traceback_parm_keep(array("show","format","page","upload","item"));
Developers may wish to debug the UI as new plugins are created. Some common debugging options are listed in the plugins core-debug-*.php.
Other debug plugins may enable additional features or display other data structures.
For web-based access, the main program is “index.php”. This is a very small program that loads all plugins and calls one plugin based on the URI “mod=” parameter. With this main program, the output type is “HTML”. For example:
http://server/ui/
is the same as
http://server/ui/?mod=Default
The module 'Default' is provided by ui/plugins/ui-default.php.
The front of the UI does not need to be complicated. For example, there are a variety of command-line tools that simply operate as a front-end to the UI. As an example, there is a command-line program called “fossjobs”. This allows the user to schedule jobs for a particular upload. The actual program loads all plugins and accesses the “Agents” menu to get the list of available jobs. Depending on the command-line parameter, it also calls the appropriate plugin to schedule the task. With this main program, plugin Output() function is not even used.
There are a couple of things that can be done to enhance this system: Currently, every plugin and common code file is loaded every time the system is accessed. This isn't a huge problem right now, but as the system expands, this could be a performance hit. Rather than dynamically registering with the menu and plugin structures, these may become standalone tables for faster access.