diff --git a/.gitignore b/.gitignore index 5bf21e1c9..809a02e39 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ css/custom.css # file marking a development version .hrm_devel_version + +# Composer's vendor folder +/vendor/ diff --git a/README.md b/README.md index b57353ced..fd6916568 100644 --- a/README.md +++ b/README.md @@ -6,3 +6,35 @@ The Huygens Remote Manager is an open-source, efficient, multi-user web-based in For more information please see: * [HRM project website](http://huygens-rm.org/) * [HRM documentation on ReadTheDocs.org](http://huygens-remote-manager.readthedocs.org/en/latest/) + * [HRM API](http://api.huygens-rm.org/html/index.html) + +Download and install dependences for development +------------------------------------------------ + +In the console, run: + +```bash +$ cd $HRM_ROOT +$ ./setup/setup_devel.sh +``` + +This will update composer, and download and install all third-party libraries used for development. Please notice that the the development dependencies are way more than those needed for release (see below). + +Package an HRM release +---------------------- + +In the console, run: + +```bash +$ cd $HRM_ROOT +$ ./setup/package_release.sh workdir archive_name +``` + +This will update composer, download and install all third-party libraries necessary for the release version of HRM, and then package everything into a zip file ready for distribution. + +Example: + +```bash +$ cd $HRM_ROOT +$ ./setup/package_release.sh /tmp ~/hrm_3.4.0.zip +``` diff --git a/aberration_correction.php b/aberration_correction.php index c99d73232..c1d7b63d0 100644 --- a/aberration_correction.php +++ b/aberration_correction.php @@ -2,12 +2,18 @@ // This file is part of the Huygens Remote Manager // Copyright and license notice: see license.txt -require_once("./inc/User.inc.php"); -require_once("./inc/Parameter.inc.php"); -require_once("./inc/Setting.inc.php"); -require_once("./inc/Util.inc.php"); -require_once("./inc/System.inc.php"); -require_once("./inc/wiki_help.inc.php"); +use hrm\DatabaseConnection; +use hrm\Nav; +use hrm\param\AberrationCorrectionMode; +use hrm\param\AdvancedCorrectionOptions; +use hrm\param\base\Parameter; +use hrm\param\CoverslipRelativePosition; +use hrm\param\ImageFileFormat; +use hrm\param\PerformAberrationCorrection; +use hrm\param\PSFGenerationDepth; + +require_once dirname(__FILE__) . '/inc/bootstrap.php'; + /* ***************************************************************************** * @@ -18,7 +24,8 @@ session_start(); if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) { - header("Location: " . "login.php"); exit(); + header("Location: " . "login.php"); + exit(); } $message = ""; @@ -31,11 +38,12 @@ /* In this page, all parameters are required! */ $parameterNames = $_SESSION['setting']->correctionParameterNames(); $db = new DatabaseConnection(); -foreach ( $parameterNames as $name ) { - $parameter = $_SESSION['setting']->parameter( $name ); - $confidenceLevel = $db->getParameterConfidenceLevel( '', $name ); - $parameter->setConfidenceLevel( $confidenceLevel ); - $_SESSION['setting']->set( $parameter ); +foreach ($parameterNames as $name) { + /** @var Parameter $parameter */ + $parameter = $_SESSION['setting']->parameter($name); + $confidenceLevel = $db->getParameterConfidenceLevel('', $name); + $parameter->setConfidenceLevel($confidenceLevel); + $_SESSION['setting']->set($parameter); } /* ***************************************************************************** @@ -44,14 +52,15 @@ * **************************************************************************** */ -if ( $_SESSION['setting']->checkPostedAberrationCorrectionParameters( $_POST ) ) { - $saved = $_SESSION['setting']->save(); - $message = $_SESSION['setting']->message(); - if ($saved) { - header("Location: select_parameter_settings.php" ); exit(); - } +if ($_SESSION['setting']->checkPostedAberrationCorrectionParameters($_POST)) { + $saved = $_SESSION['setting']->save(); + $message = $_SESSION['setting']->message(); + if ($saved) { + header("Location: select_parameter_settings.php"); + exit(); + } } else { - $message = $_SESSION['setting']->message(); + $message = $_SESSION['setting']->message(); } @@ -61,9 +70,9 @@ * **************************************************************************** */ -if ( $_SESSION['setting']->isSted() || $_SESSION['setting']->isSted3D()) { +if ($_SESSION['setting']->isSted() || $_SESSION['setting']->isSted3D()) { $back = "sted_parameters.php"; -} else if ( $_SESSION['setting']->isSpim()) { +} else if ($_SESSION['setting']->isSpim()) { $back = "spim_parameters.php"; } else { $back = "capturing_parameter.php"; @@ -76,24 +85,24 @@ **************************************************************************** */ // Javascript includes -$script = array( "settings.js", "quickhelp/help.js", - "quickhelp/aberrationCorrectionHelp.js" ); +$script = array("settings.js", "quickhelp/help.js", + "quickhelp/aberrationCorrectionHelp.js"); include("header.inc.php"); ?> - - + + Go back to previous page. - + Abort editing and go back to the image parameters selection page. All changes will be lost! - + Save and return to the image parameters selection page. @@ -101,28 +110,28 @@
-
+
-

Spherical aberration correction

+

Spherical aberration correction

-
+ - +

Do you want to enable depth-specific PSF correction? This will try to compensate for spherical aberrations introduced @@ -130,421 +139,431 @@

- parameter("PerformAberrationCorrection"); + /** @var PerformAberrationCorrection $parameterPerformAberrationCorrection */ + $parameterPerformAberrationCorrection = + $_SESSION['setting']->parameter("PerformAberrationCorrection"); - ?> + ?>
+ echo $parameterPerformAberrationCorrection->confidenceLevel(); + ?>" + onmouseover="changeQuickHelp( 'enable' );"> - - ? + ? enable depth-dependent PSF correction? -

confidenceLevel(); ?>"> -   -

+   +

- + -value( ) == 1) - $visibility = " style=\"display: block\""; + + $visibility = " style=\"display: none\""; + if ($parameterPerformAberrationCorrection->value() == 1) + $visibility = " style=\"display: block\""; -
> + ?> -

For depth-dependent correction to work properly, you have to specify - the relative position of the coverslip with respect to the first - acquired plane of the dataset. -

- - parameter("CoverslipRelativePosition"); - - ?> - -
- - - - ? - - specify sample orientation - +
> - - -

-   -

- -
- -
- - - -value( ) == 1) - $visibility = " style=\"display: block\""; - -?> - -
> +
- + + ? + + specify sample orientation + + + - AberrationCorrectionMode +

+   +

- ***************************************************************************/ +
- $parameterAberrationCorrectionMode = - $_SESSION['setting']->parameter("AberrationCorrectionMode"); +
- ?> + -

At this point the HRM has enough information to perform depth-dependent - aberration correction. Please notice that in certain circumstances, - the automatic correction scheme might generate artifacts in the result. - If this is the case, please choose the advanced correction mode. -

+ confidenceLevel(); ?>" - onmouseover="javascript:changeQuickHelp( 'mode' );" > + $visibility = " style=\"display: none\""; + if ($parameterPerformAberrationCorrection->value() == 1) + $visibility = " style=\"display: block\""; - - - ? - - correction mode - + ?> - - -

-   -

- - - -
- - - -value( ) == 1) && - ($parameterAberrationCorrectionMode->value( ) == "advanced") ) - $visibility = " style=\"display: block\""; - -?> - -
> - - At this point the HRM has enough information to perform + depth-dependent + aberration correction. Please notice that in certain + circumstances, + the automatic correction scheme might generate artifacts in the + result. + If this is the case, please choose the advanced correction mode. + + +
+ + + + ? + + correction mode + + + - AdvancedCorrectionOptions +

+   +

- ***************************************************************************/ +
- $parameterAdvancedCorrectionOptions = - $_SESSION['setting']->parameter("AdvancedCorrectionOptions"); +
- ?> + -

Here you can choose an advanced correction scheme.

+ confidenceLevel(); ?>" - onmouseover="javascript:changeQuickHelp( 'advanced' );" > + $visibility = " style=\"display: none\""; + if (($parameterPerformAberrationCorrection->value() == 1) && + ($parameterAberrationCorrectionMode->value() == "advanced") + ) + $visibility = " style=\"display: block\""; - - - ? - - advanced correction scheme - + ?> - - -Here you can choose an advanced correction scheme. -$visibility = " style=\"display: none\""; -if ( ($parameterPerformAberrationCorrection->value( ) == 1) && - ($parameterAberrationCorrectionMode->value( ) == "advanced") && - ($parameterAdvancedCorrectionOptions->value( ) == "user") ) - $visibility = " style=\"display: block\""; +
- /*************************************************************************** - - PSFGenerationDepth - - ***************************************************************************/ - - $parameterPSFGenerationDepth = - $_SESSION['setting']->parameter("PSFGenerationDepth"); - $selectedValue = $parameterPSFGenerationDepth->value(); - -?> - -
> -

Depth for PSF generation (µm): - -

-
- -

-   -

- -
- -
- -
- -
- - - -
+ + + ? + + advanced correction scheme + + + + + value() == 1) && + ($parameterAberrationCorrectionMode->value() == "advanced") && + ($parameterAdvancedCorrectionOptions->value() == "user") + ) + $visibility = " style=\"display: block\""; + + /*************************************************************************** + * + * PSFGenerationDepth + ***************************************************************************/ + + /** @var PSFGenerationDepth $parameterPSFGenerationDepth */ + $parameterPSFGenerationDepth = + $_SESSION['setting']->parameter("PSFGenerationDepth"); + $selectedValue = $parameterPSFGenerationDepth->value(); + + ?> + +
> +

Depth for PSF generation (µm): + +

+
+ +

+   +

+ + + + + +
+ +
+ + + +
- + -
+
-
+
-

Quick help

+

Quick help

-
+

The main cause of spherical aberration is a mismatch between the refractive index of the lens immersion medium and specimen embedding medium and causes the PSF to become asymmetric at depths of already a few µm. SA is especially harmful for widefield microscope deconvolution. The HRM can correct for SA automatically, but in case of very large refractive index - mismatches some artifacts can be generated. Advanced parameters + mismatches some artifacts can be generated. Advanced parameters allow for fine-tuning of the correction.

-
+
- isAdmin() ) { - ?> + isAdmin()) { + ?>
- Parameter requirements
adapted for - parameter( "ImageFileFormat" ); - echo $fileFormat->value(); - ?> - files + Parameter requirements
adapted for + parameter("ImageFileFormat"); + echo $fileFormat->value(); + ?> + files
- + -
+
-
- + $message

"; + echo "

$message

"; -?> -
+ ?> +
-
+ isAdmin()) { - $emailToUse = ''; - } else { + // E-mail address + if (UserManager::canModifyEmailAddress($edit_user)) { + + // Check that a valid e-mail address was provided if ($clean['email'] == "") { $result = False; $message = "Please fill in the email field with a valid address"; } else { $emailToUse = $clean['email']; } - } - // Group - if ($edit_user->isAdmin()) { - $groupToUse = ''; } else { + + // Use current e-mail address + $emailToUse = $edit_user->emailAddress(); + + } + + // User group + if (UserManager::canModifyGroup($edit_user)) { + + // Check that a valid group was provided if ($clean['group'] == "") { $result = False; $message = "Please fill in the group field"; } else { $groupToUse = $clean['group']; } + + } else { + + // Use current group + $groupToUse = $edit_user->userGroup(); + } // Passwords @@ -130,32 +155,57 @@ } // Update the information in the database - if ($result == True) { + if ($result == true) { - // Get the user manager - $userManager = UserManagerFactory::getUserManager($edit_user->isAdmin()); - $success = $userManager->updateUser($edit_user->isAdmin(), $edit_user->name(), - $passToUse, $emailToUse, $groupToUse); + // Update the User information + if (UserManager::canModifyEmailAddress($edit_user)) { + $edit_user->SetEmailAddress($emailToUse); + } + if (UserManager::canModifyGroup($edit_user)) { + $edit_user->SetGroup($groupToUse); + } + $success = UserManager::storeUser($edit_user, true); + + if ($success == true) { + + // Now we need to update the password (and update the success + // status). + $success &= UserManager::changeUserPassword($edit_user->name(), + $passToUse); + + } - if ($success == True) { + if (!$success) { + + $message = "Sorry, an error occurred and the user data could " . + "not be updated!"; + + } else { + + // If updating some other User setting, remove the modified + // User from the session and return to the user management page. if (isset($_SESSION['account_user'])) { - $_SESSION['account_user'] = + unset($_SESSION['account_user']); + $_SESSION['account_update_message'] = "Account details successfully modified"; header("Location: " . "user_management.php"); exit(); } else { $message = "Account details successfully modified"; - $edit_user->reload(); $_SESSION['user'] = $edit_user; header("Location: " . $_SESSION['referer']); exit(); } - } else { - $message = "Database error, please inform the administrator"; } } } +/* ***************************************************************************** + * + * DISPLAY PAGE + * + **************************************************************************** */ + include("header.inc.php"); ?> @@ -169,15 +219,15 @@ @@ -186,92 +236,117 @@
-

SelectImages Your account

+

Account Your account

-
+
isAdmin()) { - ?> - - - - - -emailAddress(); } -?> -
- - if (isset($_SESSION['account_user']) || - !$_SESSION['user']->isAdmin()) { -?> - - E-mail address: + + + - +
- - group(); } + + $somethingToChange = true; + + ?> + + + + + -
+ +
+ + +
+ + + + + -
- - -
- - - - -

 

+

- -

- -
- - + ?>
+ +
+ + +
+ +

The authentication backend does not allow the HRM to make any change!

+
+ +
+ +
@@ -287,9 +362,9 @@ class="icon save"
-$message

"; -?> + $message

"; + ?>
diff --git a/add_user_popup.php b/add_user_popup.php index febbc045a..d8da26a9c 100644 --- a/add_user_popup.php +++ b/add_user_popup.php @@ -2,17 +2,22 @@ // This file is part of the Huygens Remote Manager // Copyright and license notice: see license.txt -require_once("./inc/User.inc.php"); -require_once("./inc/Database.inc.php"); -require_once("./inc/hrm_config.inc.php"); -require_once("./inc/Mail.inc.php"); -require_once("./inc/Util.inc.php"); -require_once("./inc/Validator.inc.php"); +use hrm\Mail; +use hrm\user\proxy\ProxyFactory; +use hrm\user\UserConstants; +use hrm\user\UserManager; +use hrm\Validator; + +// Settings +global $hrm_url, $image_folder, $image_host, $email_sender, $userManagerScript; + +require_once dirname(__FILE__) . '/inc/bootstrap.php'; session_start(); if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) { - header("Location: " . "login.php"); exit(); + header("Location: " . "login.php"); + exit(); } $message = ""; @@ -30,35 +35,46 @@ * */ - // Here we store the cleaned variables - $clean = array( +// Here we store the cleaned variables +$clean = array( "username" => "", - "email" => '', - "group" => "", - "pass1" => "", - "pass2" => "", - "note" => "" ); - - // Username - if ( isset( $_POST["username"] ) ) { - if ( Validator::isUsernameValid( $_POST["username"] ) ) { - $clean["username"] = $_POST["username"]; + "email" => '', + "group" => "", + "authMode" => "", + "informUserOnCreation" => false); + +// Username +if (isset($_POST["username"])) { + if (Validator::isUserNameValid($_POST["username"])) { + $clean["username"] = $_POST["username"]; } - } +} - // Email - if ( isset( $_POST["email"] ) ) { - if ( Validator::isEmailValid( $_POST["email"] ) ) { - $clean["email"] = $_POST["email"]; +// Email +if (isset($_POST["email"])) { + if (Validator::isEmailValid($_POST["email"])) { + $clean["email"] = $_POST["email"]; } - } +} - // Group name - if ( isset( $_POST["group"] ) ) { - if ( Validator::isGroupNameValid( $_POST["group"] ) ) { - $clean["group"] = $_POST["group"]; +// Group name +if (isset($_POST["group"])) { + if (Validator::isGroupNameValid($_POST["group"])) { + $clean["group"] = $_POST["group"]; } - } +} + +// Authentication mode +$clean["authMode"] = ProxyFactory::getDefaultAuthenticationMode(); +if (isset($_POST["authMode"])) { + $clean["authMode"] = $_POST["authMode"]; +} + +// Inform the user on creation? +$clean["informUserOnCreation"] = false; +if (isset($_POST["inform"]) && $_POST["inform"] == "Yes") { + $clean["informUserOnCreation"] = true; +} /* * @@ -66,73 +82,81 @@ * */ -// TODO refactor from here +// Add the user if (isset($_POST['add'])) { - //$user = new User(); - //$user->setName( $clean['username'] ); - - if ( $clean["username"] != "" ) { - if ( $clean["email"] != "" ) { - if ($clean['group'] != "") { - $db = new DatabaseConnection(); - // Is the user name already taken? - if ($db->emailAddress($clean['username']) == "") { - $password = get_rand_id(8); - $result = $db->addNewUser( $clean["username"], - $password, $clean["email"], - $clean["group"], 'a' ); - - // TODO refactor - if ($result) { - $text = "Your account has been activated:\n\n"; - $text .= "\t Username: ".$clean["username"]."\n"; - $text .= "\t Password: ".$password."\n\n"; - $text .= "Login here\n"; - $text .= $hrm_url."\n\n"; - $folder = $image_folder . "/" . $clean["username"]; - $text .= "Source and destination folders for your images are " . - "located on server ".$image_host." under ".$folder."."; - $mail = new Mail($email_sender); - $mail->setReceiver($clean['email']); - $mail->setSubject('Account activated'); - $mail->setMessage($text); - $mail->send(); - //$user->setName( '' ); - $message = "New user successfully added to the system"; - shell_exec("$userManagerScript create \"" . $clean["username"] . "\"" ); - $added = True; - } - else $message = "Database error, please inform the person in charge"; + + if ($clean["username"] == "") { + $message = "Please provide a user name!"; + } else if ($clean["email"] == "") { + $message = "Please provide a valid email address!"; + } else if ($clean['group'] == "") { + $message = "Please a group!"; + } else { + + // Make sure that there is no user with same name + if (UserManager::existsUserWithName($clean['username'])) { + $name = $clean['username']; + $message = "Sorry, a user with name $name exists already!"; + } else { + + // Add the user + // TODO: add institution and authentication mode! + $institution_id = 1; + $password = UserManager::generateRandomPlainPassword(); + $result = UserManager::createUser($clean["username"], + $password, $clean["email"], $clean["group"], $institution_id, + ProxyFactory::getDefaultAuthenticationMode(), + UserConstants::ROLE_USER, UserConstants::STATUS_ACTIVE); + + // TODO refactor + if ($result) { + if ($clean["informUserOnCreation"]) { + $text = "Your account has been activated:\n\n"; + $text .= "\t Username: " . $clean["username"] . "\n"; + if ($clean["authMode"] == "integrated") { + $text .= "\t Password: " . $password . "\n\n"; + } else { + $text .= "\t Password: Use your " . ProxyFactory::getDefaultProxy()->friendlyName() . + " password to login.\n\n"; + } + $text .= "Login here\n"; + $text .= $hrm_url . "\n\n"; + $folder = $image_folder . "/" . $clean["username"]; + $text .= "Source and destination folders for your images are " . + "located on server " . $image_host . " under " . $folder . "."; + $mail = new Mail($email_sender); + $mail->setReceiver($clean['email']); + $mail->setSubject('Account activated'); + $mail->setMessage($text); + $mail->send(); + } + $message = "New user successfully added to the system."; + shell_exec("$userManagerScript create \"" . $clean["username"] . "\""); + $added = True; + } else { + $message = "Sorry, the user could not be registered. Please contact your administrator."; + } } - else $message = "This user name is already in use"; - } - else $message = "Please fill in group field"; } - else $message = "Please fill in email field with a valid address"; - } - else $message = "Please fill in name field"; } -echo ""; - ?> - - + + + Huygens Remote Manager + + +' . "\n"; + } +?> + + + + + + + + +
+ + +
+

+ Huygens Remote Manager + + +

+ +
+ + diff --git a/home.php b/home.php index be07931cc..1f6ac4b32 100644 --- a/home.php +++ b/home.php @@ -2,11 +2,13 @@ // This file is part of the Huygens Remote Manager // Copyright and license notice: see license.txt -require_once("./inc/User.inc.php"); -require_once("./inc/hrm_config.inc.php"); -require_once("./inc/Fileserver.inc.php"); -require_once("./inc/System.inc.php"); -require_once("./inc/wiki_help.inc.php"); +use hrm\Log; +use hrm\Nav; +use hrm\user\UserManager; +use hrm\user\proxy\ProxyFactory; +use hrm\Util; + +require_once dirname(__FILE__) . '/inc/bootstrap.php'; global $email_admin; global $authenticateAgainst; @@ -15,17 +17,19 @@ if (isset($_GET['exited'])) { if (session_id() && isset($_SESSION['user'])) { - report("User " . $_SESSION['user']->name() . " logged off.", 1); + Log::info("User " . $_SESSION['user']->name() . " logged off."); $_SESSION['user']->logout(); $_SESSION = array(); session_unset(); session_destroy(); } - header("Location: " . "login.php"); exit(); + header("Location: " . "login.php"); + exit(); } if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) { - header("Location: " . "login.php"); exit(); + header("Location: " . "login.php"); + exit(); } $message = ""; @@ -40,415 +44,448 @@
-
+
- -

Home  -

+ +

Home  +

- -
+ +
- isAdmin()) { + if ($_SESSION['user']->isAdmin()) { ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Users - -
- Manage users -
-

View, add, edit and delete users.

-
-
- Users -
-

User management through the HRM is disabled.

-
-
- - Account - -
- Account -
-

View and change your personal data.

-
-
- - Queue - -
- Queue status -
-

See and manage all jobs.

-
-
- - Statistics - -
- Global statistics -
-

Summary of usage statistics for all users.

-
-
- - Parameter templates - -
- Image templates -
-

Create templates for the image parameters.

-
-
- - Task parameters - -
- Restoration templates -
-

Create templates for the restoration parameters.

-
-
- - Analysis - -
- Analysis templates -
-

Create templates for the analysis parameters.

-
-
- - FileManager - -
- Raw images -
-

Upload your raw images.

-
-
- - Update - -
- Database update -
-

Update the database to the latest version.

-
-
- - System summary - -
- System summary -
-

Inspect your system.

-
-
- - GPU - -
- GPU switch -
-

Enable/Disable GPU deconvolution on all processing machines.

-
-
- -
-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Users + + +
+ Manage users +
+

View, add, edit and delete users.

+
+
+ + Account + + +
+ Account +
+

View and change your personal data.

+
+
+ + Queue + + +
+ Queue status +
+

See and manage all jobs.

+
+
+ + Statistics + + +
+ Global statistics +
+

Summary of usage statistics for all users.

+
+
+ + Parameter templates + + +
+ Image + templates +
+

Create templates for the image parameters.

+
+
+ + Task parameters + + +
+ Restoration + templates +
+

Create templates for the restoration parameters.

+
+
+ + Analysis + + +
+ Analysis + templates +
+

Create templates for the analysis parameters.

+
+
+ + FileManager + + +
+ Raw + images +
+

Upload your raw images.

+
+
+ + Update + + +
+ Database update +
+

Update the database to the latest version.

+
+
+ + System summary + + +
+ System summary +
+

Inspect your system.

+
+
+ + SERVERS + + +
+ Servers & GPUs +
+

Add servers and GPU cards for faster processing.

+
+
+   + +
+   +
+
- - - - - - - - - - - - - 0 ) { - if ( $_SESSION['numberjobadded'] == 1 ) { - $str = "1 job"; - } else { - $str = $_SESSION['numberjobadded'] . " jobs"; - } - unset( $_SESSION['numberjobadded'] ); +
- - Jobs - -
- Start a job -
-

Create and start restoration and analysis jobs.

-
-
- - Queue - -
+ + + + + + + + + + + + 0 + ) { + if ($_SESSION['numberjobadded'] == 1) { + $str = "1 job"; + } else { + $str = $_SESSION['numberjobadded'] . " jobs"; + } + unset($_SESSION['numberjobadded']); ?> - numberOfJobsInQueue(); - if ( $jobsInQueue == 0 ) { - $str = 'no jobs'; - } elseif ( $jobsInQueue == 1 ) { - $str = '1 job'; + $jobsInQueue = UserManager::numberOfJobsInQueue($_SESSION['user']->name()); + if ($jobsInQueue == 0) { + $str = 'no jobs'; + } elseif ($jobsInQueue == 1) { + $str = '1 job'; } else { - $str = '' .$jobsInQueue . ' jobs'; - } + $str = '' . $jobsInQueue . ' jobs'; + } ?> - + + + + + + + + + + + + + + + + + + + + + + + name()) == "integrated") { + ?> + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ + Jobs + + +
+ Start a job +
+

Create and start restoration and analysis jobs.

+
+
+ + Queue + + +
- Queue status -
-
-

See all jobs.
- You have +

+
+ Queue status +
+
+

See all jobs.
+ You have $str "; ?> - in the queue.

+ in the queue.

+
+
+ + Raw images + + +
+ Raw + images +
+

Upload raw images to deconvolve.

+
+
+ + Results + + +
+ Results +
+

Inspect and download your restored data and analysis + results.

+
+
+ + Statistics + + +
+ Statistics +
+

Summary of your usage statistics.

+
+
+ + Account + + +
+ Account +
+

View and change your personal data.

  
- - Raw images - -
- Raw images -
-

Upload raw images to deconvolve.

-
-
- - Results - -
- Results -
-

Inspect and download your restored data and analysis results.

-
-
- - Statistics - -
- Statistics -
-

Summary of your usage statistics.

-
-
- - Account - -
- Account -
-

View and change your personal data.

-
-
  
+ ?> + + + + + + + } + ?> -
+
" + - "\"Error\"" + - "  Error: could not retrieve version " + - "information!

"); - $("#update").show(); + updateDiv.html("

" + + "\"Error\"" + + "  Error: could not retrieve version " + + "information!

"); + updateDiv.show(); return; } if (response.newerVersionExist === "false") { - $("#update").html("

" + - "\"Latest" + - "  Congratulations! You are running the " + - "latest version of the HRM!

"); - $("#update").show(); + updateDiv.html("

" + + "\"Latest" + + "  Congratulations! You are running the " + + "latest version of the HRM!

"); + updateDiv.show(); return; } - $("#update").html( - "

" + + updateDiv.html( + "

" + "" + "\"New" + "  A newer version of the HRM (" + response.newVersion + ") is available!

"); - $("#update").on("click", function() { + updateDiv.on("click", function () { openWindow("http://huygens-rm.org/home/?q=node/4"); }); - $("#update").show(); - return; + updateDiv.show(); + } ) } // Hide the "update" div in the beginning - $(document).ready(function() { + $(document).ready(function () { $("#update").hide(); }); @@ -510,21 +548,23 @@ function checkForUpdates() { ?> // Update job information - $(document).ready(function() { + $(document).ready(function () { // Fill in the information about jobs in the queue as soon as the // page is ready update(); // Set up timer for the repeated update - var interval = window.setInterval(function() { update(); }, 5000); + window.setInterval(function () { + update(); + }, 5000); // Function that queries the server via an Ajax call function update() { ajaxGetNumberOfUserJobsInQueue( - 'jobsInQueue', - '

See all jobs.
You have ', - ' in the queue.

'); + 'jobsInQueue', + '

See all jobs.
You have ', + ' in the queue.

'); } }); diff --git a/image_format.php b/image_format.php index a0beb8158..9db1ce4f6 100644 --- a/image_format.php +++ b/image_format.php @@ -2,11 +2,14 @@ // This file is part of the Huygens Remote Manager // Copyright and license notice: see license.txt -require_once("./inc/User.inc.php"); -require_once("./inc/Parameter.inc.php"); -require_once("./inc/Setting.inc.php"); -require_once("./inc/System.inc.php"); -require_once("./inc/wiki_help.inc.php"); +use hrm\DatabaseConnection; +use hrm\Nav; +use hrm\param\base\Parameter; +use hrm\setting\ParameterSetting; +use hrm\Util; + +require_once dirname(__FILE__) . '/inc/bootstrap.php'; + /* ***************************************************************************** * @@ -16,16 +19,18 @@ session_start(); -if ( !isset( $_SESSION[ 'user' ] ) || !$_SESSION[ 'user' ]->isLoggedIn() ) { - header("Location: " . "login.php"); exit(); +if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) { + header("Location: " . "login.php"); + exit(); } if (isset($_GET['home'])) { - header("Location: " . "home.php"); exit(); + header("Location: " . "home.php"); + exit(); } -if ( !isset( $_SESSION[ 'setting' ] ) ) { - $_SESSION['setting'] = new ParameterSetting(); +if (!isset($_SESSION['setting'])) { + $_SESSION['setting'] = new ParameterSetting(); } $message = ""; @@ -42,11 +47,11 @@ */ $parameterNames = $_SESSION['setting']->imageParameterNames(); $db = new DatabaseConnection(); -foreach ( $parameterNames as $name ) { - $parameter = $_SESSION['setting']->parameter( $name ); - $confidenceLevel = $db->getParameterConfidenceLevel( '', $name ); - $parameter->setConfidenceLevel( $confidenceLevel ); - $_SESSION['setting']->set( $parameter ); +foreach ($parameterNames as $name) { + $parameter = $_SESSION['setting']->parameter($name); + $confidenceLevel = $db->getParameterConfidenceLevel('', $name); + $parameter->setConfidenceLevel($confidenceLevel); + $_SESSION['setting']->set($parameter); } /* ***************************************************************************** @@ -55,17 +60,18 @@ * **************************************************************************** */ -if ( $_SESSION[ 'setting' ]->checkPostedImageParameters( $_POST ) ) { +if ($_SESSION['setting']->checkPostedImageParameters($_POST)) { - // Now we force all variable channel parameters to have the correct number - // of channels - $_SESSION[ 'setting' ]->setNumberOfChannels( - $_SESSION[ 'setting']->numberOfChannels( ) ); + // Now we force all variable channel parameters to have the correct number + // of channels + $_SESSION['setting']->setNumberOfChannels( + $_SESSION['setting']->numberOfChannels()); - // Continue to the next page - header("Location: " . "microscope_parameter.php"); exit(); + // Continue to the next page + header("Location: " . "microscope_parameter.php"); + exit(); } else { - $message = $_SESSION['setting']->message(); + $message = $_SESSION['setting']->message(); } /* ***************************************************************************** @@ -75,289 +81,295 @@ **************************************************************************** */ // Javascript includes -$script = array( "settings.js", "quickhelp/help.js", "quickhelp/imageFormat.js"); +$script = array("settings.js", "quickhelp/help.js", "quickhelp/imageFormat.js"); include("header.inc.php"); ?> - - + + Abort editing and go back to the image parameters selection page. All changes will be lost! - + Continue to next page. - + -
- -

Number of channels and PSF modality

+
-
+

Number of channels and PSF modality

-

How many channels (wavelengths) in your datasets?

- - - /*************************************************************************** +

How many channels (wavelengths) in your datasets?

- NumberOfChannels + parameter("NumberOfChannels"); + $parameterNumberOfChannels = + $_SESSION['setting']->parameter("NumberOfChannels"); - ?> + ?> -
+
- - - ? - - number of channels - + ? + + number of channels + -value()) echo "checked=\"checked\" "; - return ""; -} + function check($parameter, $value) + { + /** @var Parameter $parameter */ + if ($value == $parameter->value()) echo "checked=\"checked\" "; + return ""; + } -?> -
- mustProvide() ) { - $nParamRequiringReset++; + ?> +
+ mustProvide()) { + $nParamRequiringReset++; ?>
+ onmouseover="TagToTip('ttSpanReset' )" + onmouseout="UnTip()" + onclick="document.forms[0].NumberOfChannels[0].checked = true;">
getMaxChanCnt(); $i++) { + if ($i == 0) { + ?> + + + /> + getMaxChanCnt(); $i++) { - if ($i == 0) { -?> - - - /> - + } + ?> +
-
+
+

+   +

+
-
-

-   -

-
- -
- - - PointSpreadFunction + parameter("PointSpreadFunction"); + $parameterPointSpreadFunction = + $_SESSION['setting']->parameter("PointSpreadFunction"); - ?> + ?> -

Would you like to use an existing measured PSF obtained from - bead images or a theoretical PSF generated from explicitly - specified parameters?

+

Would you like to use an existing measured PSF obtained from + bead images or a theoretical PSF generated from explicitly + specified parameters?

-
+
- - - ? - - PSF - - -
- mustProvide() ) { - $nParamRequiringReset++; + ? + + PSF + + +
+ mustProvide()) { + $nParamRequiringReset++; ?>
+ onmouseover="TagToTip('ttSpanReset' )" + onmouseout="UnTip()" + onclick="document.forms[0].PointSpreadFunction[0].checked = true;">
- - - value() == - "theoretical") - echo "checked=\"checked\""?> /> - + value() == + "theoretical" + ) + echo "checked=\"checked\"" ?> /> + - ? - - Theoretical - value() == - "measured") - echo "checked=\"checked\"" ?> - /> - + + Theoretical + value() == + "measured" + ) + echo "checked=\"checked\"" ?> + /> + - ? - - Measured -
- -
-

-   -

- -
-
- -
- - - + ? + + Measured +
+ +
+

+   +

+
+
+ +
+ + + +
-
+
- + - 0 ) { + 0) { ?> - + Click to unselect all options. + } + ?> -
+
-
+
-
-

Quick help

+
+

Quick help

-
-

Here you are asked to define the number of channels in your - data and whether you want to use a theoretical PSF or a - measured PSF you distilled with the Huygens Software.

-
+
+

Here you are asked to define the number of channels in your + data and whether you want to use a theoretical PSF or a + measured PSF you distilled with the Huygens Software.

+
- isAdmin() ) { - ?> + isAdmin()) { + ?>
- Parameter requirements
adapted for - parameter( "ImageFileFormat" ); - echo $fileFormat->value(); - ?> - files + Parameter requirements
adapted for + parameter("ImageFileFormat"); + echo $fileFormat->value(); + ?> + files
- + -
+
-
- + $message

"; + echo "

$message

"; -?> -
+ ?> +
-
+ connection = ADONewConnection($db_type); - $this->connection->Connect($db_host, $db_user, $db_password, $db_name); - - // Set the parameter name dictionary - $this-> parameterNameDictionary = array( - "CCDCaptorSizeX" => "sampleSizesX", // In HRM there is no distinction between x and y pixel size - "ZStepSize" => "sampleSizesZ", - "TimeInterval" => "sampleSizesT", - "PinholeSize" => "pinhole", - "NumberOfChannels" => "chanCnt", - "PinholeSpacing" => "pinholeSpacing", - "ExcitationWavelength" => "lambdaEx", - "EmissionWavelength" => "lambdaEm", - "MicroscopeType" => "mType", - "NumericalAperture" => "NA", - "ObjectiveType" => "RILens", - "SampleMedium" => "RIMedia", - "unused1" => "iFacePrim", // PSFGenerationDepth? - "unused2" => "iFaceScnd", - "unused3" => "imagingDir", - "unused4" => "objQuality", - "unused5" => "photonCnt", - "unused6" => "exBeamFill", - "StedDepletionMode" => "stedMode", - "StedWavelength" => "stedLambda", - "StedSaturationFactor" => "stedSatFact", - "StedImmunity" => "stedImmunity", - "Sted3D" => "sted3D"); - } - - /*! - \brief Checks whether a connection to the DB is possible - \return true if the connection is possible, false otherwise - */ - public function isReachable() { - global $db_type; - global $db_host; - global $db_name; - global $db_user; - global $db_password; - $connection = ADONewConnection($db_type); - $result = $connection->Connect($db_host, $db_user, $db_password, $db_name); - return $result; - } - - /*! - \brief Returns the type of the database (mysql, ...) - \return type of the database (e.g. postgresql) - */ - public function type() { - global $db_type; - return $db_type; - } - - /*! - \brief Attempts to get the version of the underlying database - \return version of the database (e.g. 2.2.14) - */ - public function version() { - try { - $query = "SELECT version( );"; - $version = $this->queryLastValue($query); - } catch (Exception $e) { - $version = "Could not get version information."; - } - return $version; - } - - /*! - \brief Returns the database host name - \return name of the database host - */ - public function host() { - global $db_host; - return $db_host; - } - - /*! - \brief Returns the database name - \return name of the database - */ - public function name() { - global $db_name; - return $db_name; - } - - /*! - \brief Returns the name of the database user - \return name of the database user - */ - public function user() { - global $db_user; - return $db_user; - } - - /*! - \brief Returns the password of the database user - \return password of the database user - */ - public function password() { - global $db_password; - return $db_password; - } - - /*! - \brief Returns the ADOConnection object - \return the connection object - */ - public function connection() { - return $this->connection; - } - - /*! - \brief Executes an SQL query - \param $query SQL query - \return query object - */ - public function execute($query) { - $connection = $this->connection(); - $result = $connection->Execute($query); - return $result; - } - - /*! - \brief Executes an SQL query and returns the results - \param $queryString SQL query - \return result of the query (array) - */ - public function query($queryString) { - $connection = $this->connection(); - $resultSet = $connection->Execute($queryString); - if (!$resultSet) { - return False; - } - $rows = $resultSet->GetRows(); - return $rows; - } - - /*! - \brief Executes an SQL query and returns the last row of the results - \param $queryString SQL query - \return last row of the result of the query (array) - */ - public function queryLastRow($queryString) { - $rows = $this->query($queryString); - if (!$rows) return False; - $result = end($rows); - return $result; - } - - /*! - \brief Executes an SQL query and returns the value in the last column of - the last row of the results - \param $queryString SQL query - \return value of the last column of the last row of the result of the query - */ - public function queryLastValue($queryString) { - $rows = $this->queryLastRow($queryString); - if (!$rows) return False; - $result = end($rows); - return $result; - } - - /*! - \brief Adds a new user to the database (all parameters are expected - to be already validated! - \param $username The name of the user - \param $password Password (plain) - \param $email E-mail address - \param $group Research group - \param $status Status or ID - \return $success True if success; false otherwise - */ - public function addNewUser($username, $password, $email, $group, $status) { - $query = "INSERT INTO username (name, password, email, research_group, status) ". - "VALUES ('".$username."', ". - "'".md5($password)."', ". - "'".$email."', ". - "'".$group."', ". - "'".$status."')"; - $result = $this->execute($query); - if ( $result ) { - $query = "UPDATE username SET creation_date = CURRENT_TIMESTAMP WHERE name = '". $username . "'"; - $result = $this->execute($query); - } - if ( $result ) { - return true; - } else { - return false; - } - } - - /*! - \brief Updates an existing user in the database (all parameters are - expected to be already validated!) - - Only the password can be changed for the admin user. For a normal user, - e-mail address, group and password can be updated. - - \param $isadmin True if the user is the HRM admin - \param $username The name of the user - \param $password Password (plain) - \param $email E-mail address - \param $group Research group - \return $success True if success; false otherwise - */ - public function updateExistingUser( $isadmin, $username, $password, $email = "", $group = "" ) { - // The admin user does not have a group and stores his password in the - // configuration files. The only variable is the password. - if ( $isadmin === True ) { - $query = "UPDATE username SET password = '".md5($password)."' " . - "WHERE name = '".$username."';"; - } else { - $query = "UPDATE username SET email ='".$email."', " . - "research_group ='".$group."', ". - "password = '".md5($password)."' " . - "WHERE name = '".$username."';"; - } - $result = $this->execute($query); - if ( $result ) { - return true; - } else { - return false; - } - } - - /*! - \brief Updates an existing user in the database (all parameters are - expected to be already validated!) - - The last access time will be updated as well. - - \param $username The name of the user (used to query) - \param $email E-mail address - \param $group Research group - - \return $success True if success; false otherwise - */ - public function updateUserNoPassword($username, $email, $group) { - - if ($email == "" || $group=="") { - report("User data update: e-mail and group cannot be empty! " . - "No changes to the database!", 0); - return false; - } - - // Build query - $query = "UPDATE username SET email ='" . $email . "', " . - "research_group ='" . $group . "' " . - "WHERE name = '" . $username . "';"; - - $result = $this->execute($query); - if ( $result ) { - return true; - } else { - return false; - } - } - - /*! - \brief Updates the status of an existing user in the database (username - is expected to be already validated! - \param $username The name of the user - \param $status One of 'd', 'a', ... - \return $success True if success; false otherwise - */ - public function updateUserStatus($username, $status) { - $query = "UPDATE username SET status = '".$status."' WHERE name = '".$username."'"; - $result = $this->execute($query); - if ( $result ) { - return true; - } else { - return false; - } - } - - /*! - \brief Updates the status of all non-admin users in the database - \param $status One of 'd', 'a', ... - \return $success True if success; false otherwise - */ - public function updateAllUsersStatus($status) { - $query = "UPDATE username SET status = '".$status."' WHERE name NOT LIKE 'admin'"; - $result = $this->execute($query); - if ( $result ) { - return true; - } else { - return false; - } - } - - /*! - \brief Deletes an user and all data from the database (username is - expected to be already validated! - \param $username One of 'd', 'a', ... - \return $success True if success; false otherwise - */ - public function deleteUser($username) { - if ( $username == 'admin' ) { - return false; - } - $query = "DELETE FROM username WHERE name = '".$username."'"; - $result = $this->execute($query); - if ($result) { - // delete user's settings - $query = "DELETE FROM parameter WHERE owner = '".$username."'"; - $this->execute($query); - $query = "DELETE FROM parameter_setting WHERE owner = '".$username."'"; - $this->execute($query); - $query = "DELETE FROM task_parameter WHERE owner = '".$username."'"; - $this->execute($query); - $query = "DELETE FROM task_setting WHERE owner = '".$username."'"; - $this->execute($query); - return true; - } else { - return false; - } - } - - /*! - \brief Returns the password of a given user name - \param $name Name of the user - \return password for the requested user - */ - public function passwordQueryString($name) { - $string = "select password from username where name='" . $name . "'"; - return $string; - } - - /*! - \brief Returns the e-mail address of a given user name - \param $username Name of the user - \return e-mail address for the requested user - */ - public function emailAddress($username) { - $query = "select email from username where name = '" . $username . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Saves the parameter values of the setting object into the database. - If the setting already exists, the old values are overwritten, - otherwise a new setting is created - \param $settings Settings object to be saved - \return true if saving was successful, false otherwise - */ - public function saveParameterSettings($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - $settingTable = $settings->table(); - $table = $settings->parameterTable(); - if ($settings->isDefault()) - $standard = "t"; - else - $standard = "f"; - $result = True; - if (!$this->existsSetting($settings)) { - $query = "insert into $settingTable values ('" . $user."', '"; - $query .= $name . "', '" .$standard . "')"; - $result = $result && $this->execute($query); - } - $existsAlready = $this->existsParametersFor($settings); - - foreach ($settings->parameterNames() as $parameterName) { - $parameter = $settings->parameter($parameterName); - $parameterValue = $parameter->internalValue(); - - if (is_array($parameterValue)) { - /*! Before, # was used as a separator, but the first element - with index zero was always NULL because channels started - their indexing at one. To keep backwards compatibility with - the database, we use # now as a channel marker, and even the - first channel has a # in front of its value "/" separator is - used to mark range values for signal to noise ratio. - */ - - /*! - \todo Currently there are not longer "range values" (values - separated by /). In the future they will be reintroduced. - We leave the code in place. - */ - if (is_array($parameterValue[0])) { - $maxChanCnt = $this->getMaxChanCnt(); - for ($i = 0; $i < $maxChanCnt; $i++) { - if ($parameterValue[$i] != null) { - $parameterValue[$i] = - implode("/", array_filter($parameterValue[$i])); - } - } - } - $parameterValue = "#".implode("#", $parameterValue); - } - - if (!$existsAlready) { - $query = "INSERT INTO $table VALUES ('" . $user . "', '"; - $query .= $name . "', '" . $parameterName . "', '"; - $query .= $parameterValue . "')"; - } else { - /* Check that the parameter itself exists. */ - $query = "SELECT name FROM $table WHERE owner='" . $user; - $query .= "' AND setting='" . $name; - $query .= "' AND name='" . $parameterName . "' LIMIT 1"; - $newValue = $this->queryLastValue($query); - - if ( $newValue != NULL ) { - $query = "UPDATE $table SET value = '" . $parameterValue; - $query .= "' WHERE owner='" . $user; - $query .= "' AND setting='" . $name; - $query .= "' AND name='" . $parameterName . "'"; - } else { - $query = "INSERT INTO $table VALUES ('" . $user; - $query .= "', '" . $name . "', '" . $parameterName; - $query .= "', '" . $parameterValue . "')"; - } - } - - // Accumulate the successes (or failures) of the queries. If a query - // fails, the return of $this->execute() will be === false; otherwise - // it is an ADORecordSet. - $result &= ($this->execute($query) !== false); - } - - - return $result; - } - - /*! - \brief Saves the parameter values of the setting object into the database. - If the setting already exists, the old values are overwritten, - otherwise a new setting is created - \param $settings Settings object to be saved - \param $isShare Boolean (default = False): True if the setting is to - be saved to the share table, False if it is a standard - save. - \return true if saving was successful, false otherwise -*/ - public function saveSharedParameterSettings($settings, $targetUserName) { - $owner = $settings->owner(); - $original_user = $owner->name(); - $name = $settings->name(); - $new_owner = new User(); - $new_owner->setName($targetUserName); - $settings->setOwner($new_owner); - $settingTable = $settings->sharedTable(); - $table = $settings->sharedParameterTable(); - $result = True; - if (!$this->existsSharedSetting($settings)) { - $query = "insert into $settingTable " . - "(owner, previous_owner, sharing_date, name) values " . - "('$targetUserName', '$original_user', CURRENT_TIMESTAMP, '$name')"; - $result = $result && $this->execute($query); - } - - if (!$result) { - return False; - } - - // Get the Id - $query = "select id from $settingTable where " . - "owner='$targetUserName' " . - "AND previous_owner='$original_user' " . - "AND name='$name'"; - $id = $this->queryLastValue($query); - if (! $id) { - return False; - } - - // Get the parameter names - $parameterNames = $settings->parameterNames(); - - // Add the parameters - foreach ($parameterNames as $parameterName) { - - $parameter = $settings->parameter($parameterName); - $parameterValue = $parameter->internalValue(); - - if (is_array($parameterValue)) { - // Before, # was used as a separator, but the first element with - // index zero was always NULL because channels started their indexing - // at one. To keep backwards compatibility with the database, we use - // # now as a channel marker, and even the first channel has a # in - // front of its value. - // "/" separator is used to mark range values for signal to noise ratio - - - // Special treatment for the PSF parameter. - if ($parameter->name() == "PSF") { - - // Create hard links and update paths to the PSF files - // to point to the hard-links. - $fileServer = new Fileserver($original_user); - $parameterValue = $fileServer->createHardLinksToSharedPSFs( - $parameterValue, $targetUserName); - - } - - /*! - \todo Currently there are not longer "range values" (values - separated by /). In the future they will be reintroduced. - We leave the code in place. - */ - if (is_array($parameterValue[0])) { - $maxChanCnt = $this->getMaxChanCnt(); - for ($i = 0; $i < $maxChanCnt; $i++) { - if ($parameterValue[$i] != null) { - $parameterValue[$i] = implode("/", array_filter($parameterValue[$i])); - } - } - } - $parameterValue = "#".implode("#", $parameterValue); - } - - $query = "insert into $table " . - "(setting_id, owner, setting, name, value) " . - "values ('$id', '$targetUserName', '$name', " . - "'$parameterName', '$parameterValue')"; - $result = $result && $this->execute($query); - } - - return $result; - } - - /*! - \brief Loads the parameter values for a setting and returns a copy of - the setting with the loaded parameter values. If a value starts - with # it is considered to be an array with the first value at - the index 0 - \param $settings Setting object to be loaded - \return $settings object with loaded values - */ - public function loadParameterSettings($settings) { - $user = $settings->owner(); - $user = $user->name(); - $name = $settings->name(); - $table = $settings->parameterTable(); - - foreach ($settings->parameterNames() as $parameterName) { - $parameter = $settings->parameter($parameterName); - $query = "SELECT value FROM $table WHERE owner='" . $user; - $query .= "' AND setting='" . $name . "' AND name='"; - $query .= $parameterName . "'"; - - $newValue = $this->queryLastValue($query); - - if ($newValue == NULL) { - - // See if the Parameter has a usable default - $newValue = $parameter->defaultValue( ); - if ($newValue == NULL) { - continue; - } - } - - - if ($newValue{0} == '#') { - switch($parameterName) { - case "ExcitationWavelength": - case "EmissionWavelength": - case "PinholeSize": - case "PinholeSpacing": - case "SignalNoiseRatio": - case "BackgroundOffsetPercent": - case "ChromaticAberration": - case "StedDepletionMode": - case "StedWavelength": - case "StedSaturationFactor": - case "StedImmunity": - case "Sted3D": - case "SpimExcMode": - case "SpimGaussWidth": - case "SpimCenterOffset": - case "SpimFocusOffset": - case "SpimNA": - case "SpimFill": - case "SpimDir": - case "ColocChannel": - case "ColocThreshold": - case "ColocCoefficient": - /* Extract and continue to explode. */ - $newValue = substr($newValue,1); - default: - $newValues = explode("#", $newValue); - } - - if (strcmp( $parameterName, "PSF" ) != 0 - && strpos($newValue, "/")) { - $newValue = array(); - for ($i = 0; $i < count($newValues); $i++) { - if (strpos($newValues[$i], "/")) { - $newValue[] = explode("/", $newValues[$i]); - } - else { - $newValue[] = array($newValues[$i]); - } - } - } - else { - $newValue = $newValues; - } - } - - $parameter->setValue($newValue); - $settings->set($parameter); - } - - return $settings; - } - - /*! - \brief Loads the parameter values for a setting and returns a copy of - the setting with the loaded parameter values. If a value starts - with # it is considered to be an array with the first value at - the index 0 - \param $id Setting id - \param $id Setting id - \return $settings object with loaded values - */ - public function loadSharedParameterSettings($id, $type) { - - // Get the correct objects - switch ($type) { - - case "parameter": - - $settingTable = ParameterSetting::sharedTable(); - $table = ParameterSetting::sharedParameterTable(); - $settings = new ParameterSetting(); - break; - - case "task": - - $settingTable = TaskSetting::sharedTable(); - $table = TaskSetting::sharedParameterTable(); - $settings = new TaskSetting(); - break; - - case "analysis": - - $settingTable = AnalysisSetting::sharedTable(); - $table = AnalysisSetting::sharedParameterTable(); - $settings = new AnalysisSetting(); - break; - - default: - - throw new Exception("bad value for type!"); - } - - // Get the setting info - $query = "select * from $settingTable where id=$id;"; - $response = $this->queryLastRow($query); - if (!$response) { - return NULL; - } - - // Fill the setting - $settings->setName($response["name"]); - $user = new User(); - $user->setName($response["owner"]); - $settings->setOwner($user); - - // Load from shared table - foreach ($settings->parameterNames() as $parameterName) { - $parameter = $settings->parameter($parameterName); - $query = "select value from $table where setting_id=$id and name='$parameterName'"; - $newValue = $this->queryLastValue($query); - if ($newValue == NULL) { - // See if the Parameter has a usable default - $newValue = $parameter->defaultValue( ); - if ($newValue == NULL) { - continue; - } - } - if ($newValue{0}=='#') { - switch($parameterName) { - case "ExcitationWavelength": - case "EmissionWavelength": - case "SignalNoiseRatio": - case "BackgroundOffsetPercent": - case "ChromaticAberration": - /* Extract and continue to explode. */ - $newValue = substr($newValue,1); - default: - $newValues = explode("#", $newValue); - } - - if (strcmp( $parameterName, "PSF" ) != 0 && strpos($newValue, "/")) { - $newValue = array(); - for ($i = 0; $i < count($newValues); $i++) { - //$val = explode("/", $newValues[$i]); - //$range = array(NULL, NULL, NULL, NULL); - //for ($j = 0; $j < count($val); $j++) { - // $range[$j] = $val[$j]; - //} - //$newValue[] = $range; - /*! - \todo Currently there are not longer "range values" (values - separated by /). In the future they will be reintroduced. - We leave the code in place. - */ - if (strpos($newValues[$i], "/")) { - $newValue[] = explode("/", $newValues[$i]); - } - else { - $newValue[] = array($newValues[$i]); - } - } - } - else { - $newValue = $newValues; - } - } - //$shiftedNewValue = array(1 => NULL, 2 => NULL, 3 => NULL, 4 => NULL, 5 => NULL); - //if (is_array($newValue)) { - // // start array at 1 - // for ($i = 1; $i <= count($newValue); $i++) { - // $shiftedNewValue[$i] = $newValue[$i - 1]; - // } - //} - //else $shiftedNewValue = $newValue; - $parameter->setValue($newValue); - $settings->set($parameter); - } - return $settings; - } - - /*! - \brief Returns the list of shared templates with the given user. - \param $username Name of the user for whom to query for shared templates - \param $table Shared table to query - \return list of shared jobs - */ - public function getTemplatesSharedWith($username, $table) { - $query = "SELECT * FROM $table WHERE owner='$username'"; - $result = $this->query($query); - return $result; - } - - /*! - \brief Returns the list of shared templates by the given user. - \param $username Name of the user for whom to query for shared templates - \param $table Shared table to query - \return list of shared jobs - */ - public function getTemplatesSharedBy($username, $table) { - $query = "SELECT * FROM $table WHERE previous_owner='$username'"; - $result = $this->query($query); - return $result; - } - - /*! - \brief Copies the relevant rows from shared- to user- tables. - \param $id ID of the setting to be copied - \param $sourceSettingTable Setting table to copy from - \param $sourceParameterTable Parameter table to copy from - \param $destSettingTable Setting table to copy to - \param $destParameterTable Parameter table to copy to - \return True if copying was successful; false otherwise. - */ - public function copySharedTemplate($id, $sourceSettingTable, - $sourceParameterTable, $destSettingTable, $destParameterTable) { - - // Get the name of the previous owner (the one sharing the setting). - $query = "select previous_owner, owner, name from $sourceSettingTable where id=$id"; - $rows = $this->queryLastRow($query); - if (False === $rows) { - return False; - } - $previous_owner = $rows["previous_owner"]; - $owner = $rows["owner"]; - $setting_name = $rows["name"]; - - // Compose the new name of the setting - $out_setting_name = $previous_owner . "_" . $setting_name; - - // Check if a setting with this name already exists in the target tables - $query = "select name from $destSettingTable where " . - "name='$out_setting_name' and owner='$owner'"; - if ($this->queryLastValue($query)) { - - // The setting already exists; we try adding numerical indices - $n = 1; $original_out_setting_name = $out_setting_name; - while (1) { - - $test_name = $original_out_setting_name . "_" . $n++; - $query = "select name from $destSettingTable where name='$test_name' and owner='$owner'"; - if (! $this->queryLastValue($query)) { - $out_setting_name = $test_name; - break; - } - } - - } - - // Get all rows from source table for given setting id - $query = "select * from $sourceParameterTable where setting_id=$id"; - $rows = $this->query($query); - if (count($rows) == 0) { - return False; - } - - // Now add the rows to the destination table - $ok = True; - $record = array(); - $this->connection->BeginTrans(); - foreach ($rows as $row) { - $record["owner"] = $row["owner"]; - $record["setting"] = $out_setting_name; - $record["name"] = $row["name"]; - - // PSF files must be processed differently - if ($record["name"] == "PSF") { - - // Instantiate a Fileserver object for the target user - $fileserver = new Fileserver($owner); - - // Get the array of PSF names - $values = $row["value"]; - if ($values[0] == "#") { - $values = substr($values, 1); - } - $psfFiles = explode('#', $values); - - // Create hard-links to the target user folder - $newPSFFiles = $fileserver->createHardLinksFromSharedPSFs( - $psfFiles, $owner, $previous_owner); - - // Update the entries for the database - $record["value"] = "#" . implode('#', $newPSFFiles); - - } else { - - $record["value"] = $row["value"]; - - } - - $insertSQL = $this->connection->GetInsertSQL($destParameterTable, - $record); - $status = $this->connection->Execute($insertSQL); - $ok &= !(false === $status); - if (! $ok) { - break; - } - } - - // If everything went okay, we commit the transaction; otherwise we roll - // back - if ($ok) { - $this->connection->CommitTrans(); - } else { - $this->connection->RollbackTrans(); - return False; - } - - // Now add the setting to the setting table - $query = "select * from $sourceSettingTable where id=$id"; - $rows = $this->query($query); - if (count($rows) != 1) { - return False; - } - - $ok = True; - $this->connection->BeginTrans(); - $record = array(); - $row = $rows[0]; - $record["owner"] = $row["owner"]; - $record["name"] = $out_setting_name; - $record["standard"] = 'f'; - $insertSQL = $this->connection->GetInsertSQL($destSettingTable, - $record); - $status = $this->connection->Execute($insertSQL); - $ok &= !(false === $status); - - if ($ok) { - $this->connection->CommitTrans(); - } else { - $this->connection->RollbackTrans(); - return False; - } - - // Now we can delete the records from the source tables. Even if it - // if it fails we do not roll back, since the parameters were copied - // successfully. - - // Delete setting entry - $query = "delete from $sourceSettingTable where id=$id"; - $status = $this->connection->Execute($query); - if (false === $status) { - return False; - } - - // Delete parameter entries - $query = "delete from $sourceParameterTable where setting_id=$id"; - $status = $this->connection->Execute($query); - if (false === $status) { - return False; - } - - return True; - } - - /*! - \brief Delete the relevant rows from the shared tables. - \param $id ID of the setting to be deleted - \param $sourceSettingTable Setting table to copy from - \param $sourceParameterTable Parameter table to copy from - \return True if deleting was successful; false otherwise. - */ - public function deleteSharedTemplate($id, $sourceSettingTable, - $sourceParameterTable) { - - // Initialize success - $ok = True; - - // Delete shared PSF files if any exist - if ($sourceParameterTable == "shared_parameter") { - $query = "select value from $sourceParameterTable where setting_id=$id and name='PSF'"; - $psfFiles = $this->queryLastValue($query); - if (NULL != $psfFiles && $psfFiles != "#####") { - if ($psfFiles[0] == "#") { - $psfFiles = substr($psfFiles, 1); - } - - // Extract PSF file paths from the string - $psfFiles = explode("#", $psfFiles); - - // Delete them - Fileserver::deleteSharedFSPFilesFromBuffer($psfFiles); - } - } - - // Delete setting entry - $query = "delete from $sourceSettingTable where id=$id"; - $status = $this->connection->Execute($query); - $ok &= !(false === $status); - - // Delete parameter entries - $query = "delete from $sourceParameterTable where setting_id=$id"; - $status = $this->connection->Execute($query); - $ok &= !(false === $status); - - return $ok; - } - - /*! - \brief Updates the default entry in the database according to the default - value in the setting - \param $settings Settings object to be used to update the default - \return query result - */ - public function updateDefault($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - if ($settings->isDefault()) - $standard = "t"; - else - $standard = "f"; - $table = $settings->table(); - $query = "update $table set standard = '" . $standard . "' where owner='" . $user . "' and name='" . $name . "'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Deletes the setting and all its parameter values from the database - \param $settings Settings object to be used to delete all entries from - the database - \return true if the setting and all parameters were deleted from the - database; false otherwise - */ - public function deleteSetting($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - $result = True; - $table = $settings->parameterTable(); - $query = "delete from $table where owner='" . $user . "' and setting='" . $name ."'"; - $result = $result && $this->execute($query); - if (!$result) { - return FALSE; - } - $table = $settings->table(); - $query = "delete from $table where owner='" . $user . "' and name='" . $name ."'"; - $result = $result && $this->execute($query); - return $result; - } - - /*! - \brief Checks whether parameters are already stored for a given setting - \param $settings Settings object to be used to check for existance in - the database - \return true if the parameters exist in the database; false otherwise - */ - public function existsParametersFor($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - $table = $settings->parameterTable(); - $query = "select name from $table where owner='" . $user . "' and setting='" . $name ."' LIMIT 1"; - $result = True; - if (!$this->queryLastValue($query)) { - $result = False; - } - return $result; - } - - /*! - \brief Checks whether parameters are already stored for a given shared - setting - \param $settings Settings object to be used to check for existence in - the database - \return true if the parameters exist in the database; false otherwise - */ - public function existsSharedParametersFor($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - $table = $settings->sharedParameterTable(); - $query = "select name from $table where owner='" . $user . "' and setting='" . $name ."' LIMIT 1"; - $result = True; - if (!$this->queryLastValue($query)) { - $result = False; - } - return $result; - } - - /*! - \brief Checks whether settings exist in the database for a given owner - \param $settings Settings object to be used to check for existance in - the database (the name of the owner must be set in the - settings) - \return true if the settings exist in the database; false otherwise - */ - public function existsSetting($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - $table = $settings->table(); - $query = "select standard from $table where owner='" . $user . "' and name='" . $name ."' LIMIT 1"; - $result = True; - if (!$this->queryLastValue($query)) { - $result = False; - } - return $result; - } - - /*! - \brief Checks whether settings exist in the database for a given owner - \param $settings Settings object to be used to check for existence in - the database (the name of the owner must be set in the - settings) - \return true if the settings exist in the database; false otherwise - */ - public function existsSharedSetting($settings) { - $owner = $settings->owner(); - $user = $owner->name(); - $name = $settings->name(); - $table = $settings->sharedTable(); - $query = "select standard from $table where owner='" . $user . "' and name='" . $name ."' LIMIT 1"; - $result = True; - if (!$this->queryLastValue($query)) { - $result = False; - } - return $result; - } - - /*! - \brief Adds all files for a given job id and user to the database - \param $id Job id - \param $owner Name of the user that owns the job - \param $files Array of file names - \return true if the job files could be saved successfully; false otherwise - */ - public function saveJobFiles($id, $owner, $files, $autoseries) { - $result = True; - $username = $owner->name(); - $sqlAutoSeries = ""; - foreach ($files as $file) { - if (strcasecmp($autoseries, "TRUE") == 0 || strcasecmp($autoseries, "T") == 0) { - $sqlAutoSeries = "T"; - } - $query = "insert into job_files values ('" . $id ."', '" . $username ."', '" . addslashes($file) . "', '" . $sqlAutoSeries . "')"; - $result = $result && $this->execute($query); - } - return $result; - } - - /*! - \brief Adds a job for a given job id and user to the database - \param $id Job id - \param $username Name of the user that owns the job - \return query result - */ - public function queueJob($id, $username) { - $query = "insert into job_queue (id, username, queued, status) values ('" .$id . "', '" . $username . "', NOW(), 'queued')"; - return $this->execute($query); - } - - /*! - \brief Assigns priorities to the jobs in the queue - \return true if assigning priorities was successful - */ - public function setJobPriorities( ) { - - $result = True; - - //////////////////////////////////////////////////////////////////////////// - // - // First we analyze the queue - // - //////////////////////////////////////////////////////////////////////////// - - // Get the number of users that currently have jobs in the queue - $users = $this->execute( "SELECT DISTINCT( username ) FROM job_queue;" ); - $row = $this->execute( "SELECT COUNT( DISTINCT( username ) ) FROM job_queue;" )->FetchRow( ); - $numUsers = $row[ 0 ]; - - // 'Highest' priority (i.e. lowest value) is 0 - $currentPriority = 0; - - // First, we make sure to give the highest priorities to paused and - // broken jobs - $rs = $this->execute( "SELECT id FROM job_queue WHERE status = 'broken' OR status = 'paused';" ); - if ( $rs ) { - while ( $row = $rs->FetchRow( ) ) { - - // Update the priority for current job id - $query = "UPDATE job_queue SET priority = " . $currentPriority++ . - " WHERE id = '" . $row[ 0 ] . "';"; - - $rs = $this->execute( $query ); - if ( !$rs ) { - error_log( "Could not update priority for key " . $row[ 0 ] ); - $result = False; - return $result; - } - - } - } - - // Then, we go through to running jobs - $rs = $this->execute( "SELECT id FROM job_queue WHERE status = 'started';" ); - if ( $rs ) { - while ( $row = $rs->FetchRow( ) ) { - - // Update the priority for current job id - $query = "UPDATE job_queue SET priority = " . $currentPriority++ . - " WHERE id = '" . $row[ 0 ] . "';"; - - $rs = $this->execute( $query ); - if ( !$rs ) { - error_log( "Could not update priority for key " . $row[ 0 ] ); - $result = False; - return $result; - } - } - } - - // Then we organize the queued jobs in a way that lets us then assign - // priorities easily in a second pass - $numJobsPerUser = array( ); - $userJobs = array( ); - for ( $i = 0; $i < $numUsers; $i++ ) { - // Get current username - $row = $users->FetchRow( ); - $username = $row[ 0 ]; - $query = "SELECT id - FROM job_queue, job_files - WHERE job_queue.id = job_files.job AND - job_queue.username = job_files.owner AND - job_queue.username = '" . $username . "' AND - status = 'queued' - ORDER BY job_queue.queued asc, job_files.file asc"; - $rs = $this->execute( $query ); - if ( $rs ) { - $userJobs[ $i ] = array( ); - $counter = 0; - while ( $row = $rs->FetchRow( ) ) { - $userJobs[ $i ][ $counter++ ] = $row[ 0 ]; - } - $numJobsPerUser[ $i ] = $counter; - } - } - - // Now we can assign priorities to the queued jobs -- minimum priority is 1 - // above the priorities assigned to all other types of jobs - $maxNumJobs = max( $numJobsPerUser ); - for ( $j = 0; $j < $maxNumJobs; $j++ ) { - for ( $i = 0; $i < $numUsers; $i++ ) { - if ( $j < count( $userJobs[ $i ] ) ) { - // Update the priority for current job id - $query = "UPDATE job_queue SET priority = " . - $currentPriority ." WHERE id = '" . - $userJobs[ $i ][ $j ] . "';"; - - $rs = $this->execute( $query ); - if ( !$rs ) { - error_log( "Could not update priority for key " . $userJobs[ $i ][ $j ] ); - $result = False; - return $result; - } - $currentPriority++; - } - } - } - - // We can now return true - return $result; - } - - /*! - \brief Logs job information in the statistics table - \param $job Job object whose information is to be logged in the - database - \param $startTime Job start time - \return void - */ - public function updateStatistics($job, $startTime) { - - $desc = $job->description(); - $parameterSetting = $desc->parameterSetting(); - $taskSetting = $desc->taskSetting(); - $analysisSetting = $desc->analysisSetting(); - - $stopTime = date("Y-m-d H:i:s"); - $id = $desc->id(); - $user = $desc->owner(); - $owner = $user->name(); - $group = $user->userGroup($owner); - - $parameter = $parameterSetting->parameter('ImageFileFormat'); - $inFormat = $parameter->value(); - $parameter = $parameterSetting->parameter('PointSpreadFunction'); - $PSF = $parameter->value(); - $parameter = $parameterSetting->parameter('MicroscopeType'); - $microscope = $parameter->value(); - $parameter = $taskSetting->parameter('OutputFileFormat'); - $outFormat = $parameter->value(); - $parameter = $analysisSetting->parameter('ColocAnalysis'); - $colocAnalysis = $parameter->value(); - - $query = "insert into statistics values ('" . $id ."', '" . $owner ."', '" . - $group . "','" . $startTime . "', '" . $stopTime . "', '" . $inFormat . - "', '" . $outFormat . "', '" . $PSF . "', '" . - $microscope . "', '" . $colocAnalysis . "')"; - - $this->execute($query); - - } - - /*! - \brief Flattens a multi-dimensional array - \param $anArray Multi-dimensional array - \return flattened array - */ - public function flatten($anArray) { - $result = array(); - foreach ($anArray as $row) { - $result[] = end($row); - } - return $result; - } - - /*! - \brief Returns the possible values for a given parameter - \param $parameter Parameter object - \return Flattened array of possible values - */ - public function readPossibleValues($parameter) { - $name = $parameter->name(); - $query = "select value from possible_values where parameter = '" .$name . "'"; - $answer = $this->query($query); - $result = $this->flatten($answer); - return $result; - } - - /*! - \brief Returns the translated possible values for a given parameter - \param $parameter Parameter object - \return Flattened array of translated possible values - */ - public function readTranslatedPossibleValues($parameter) { - $name = $parameter->name(); - $query = "select translation from possible_values where parameter = '" .$name . "'"; - $answer = $this->query($query); - $result = $this->flatten($answer); - return $result; - } - - /*! - \brief Returns the translation of current value for a given parameter - \param $parameterName Name of the Parameter object - \param $value Value for which a thanslation should be returned - \return Translated value - */ - public function translationFor($parameterName, $value) { - $query = "select translation from possible_values where parameter = '" .$parameterName . "' and value = '" . $value . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Returns the translation of a hucore value - \param $parameterName Name of the Parameter object - \param $hucorevalue value name in HuCore - \return Expected value by HRM -*/ - public function hucoreTranslation($parameterName, $hucorevalue) { - $query = "select value from possible_values where parameter = '" .$parameterName . "' and translation = '" . $hucorevalue . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Returns an array of all file extensions - \return Array of file extensions - */ - public function allFileExtensions( ) { - $query = "select distinct extension from file_extension"; - $answer = $this->query($query); - $result = $this->flatten($answer); - return $result; - } - - /*! - \brief Returns an array of all extensions for multi-dataset files - \return Array of file extensions for multi-dataset files - */ - public function allMultiFileExtensions( ) { - $query = "SELECT name FROM file_format, file_extension - WHERE file_format.name = file_extension.file_format - AND file_format.ismultifile LIKE 't'"; - $answer = $this->query($query); - $result = $this->flatten($answer); - return $result; - } - - /*! - \brief Returns an array of file extensions associated to a given file format - \param $imageFormat File format - \return Array of file extensions - */ - public function fileExtensions($imageFormat) { - $query = "select distinct extension from file_extension where file_format = '" . $imageFormat . "'"; - $answer = $this->query($query); - $result = $this->flatten($answer); - return $result; - } - - /*! - \brief Returns all restrictions for a given numerical parameter - \param $parameter Parameter (object) - \return Array of restrictions - */ - public function readNumericalValueRestrictions($parameter) { - $name = $parameter->name(); - $query = "select min, max, min_included, max_included, standard from boundary_values where parameter = '" .$name . "'"; - $result = $this->queryLastRow($query); - if ( !$result ) { - $result = array( null, null, null, null, null ); - } - return $result; - } - - /*! - \brief Returns the file formats that fit the conditions expressed by the - parameters. - \param $isSingleChannel Set whether the file format must be single - channel (True), multi channel (False) or if - it doesn't matter (NULL) - \param $isVariableChannel Set whether the number of channels must be - variable (True), fixed (False) or if it - doesn't matter (NULL) - \param $isFixedGeometry Set whether the geometry (xyzt) must be fixed - (True), variable (False) or if it doesn't - matter (NULL). - \return array of file formats - */ - public function fileFormatsWith($isSingleChannel, $isVariableChannel, $isFixedGeometry) { - $isSingleChannelValue = 'f'; - $isVariableChannelValue = 'f'; - $isFixedGeometryValue ='f'; - if ($isSingleChannel) { - $isSingleChannelValue = 't'; - } - if ($isVariableChannel) { - $isVariableChannelValue = 't'; - } - if ($isFixedGeometry) { - $isFixedGeometryValue = 't'; - } - $conditions = array(); - if ($isSingleChannel!=NULL) { - $conditions['isSingleChannel'] = $isSingleChannelValue; - } - if ($isVariableChannel!=NULL) { - $conditions['isVariableChannel'] = $isVariableChannelValue; - } - if ($isFixedGeometry!=NULL) { - $conditions['isFixedGeometry'] = $isFixedGeometryValue; - } - return $this->retrieveColumnFromTableWhere('name', 'file_format', $conditions); - } - - /*! - \brief Returns the geometries (XY, XY-time, XYZ, XYZ-time) fit the - conditions expressed by the parameters - \param $isThreeDimensional True if 3D - \param $isTimeSeries True if time-series - \return array of geometries - */ - public function geometriesWith($isThreeDimensional, $isTimeSeries) { - $isThreeDimensionalValue = 'f'; - $isTimeSeriesValue = 'f'; - if ($isThreeDimensional) { - $isThreeDimensionalValue = 't'; - } - if ($isTimeSeries) { - $isTimeSeriesValue = 't'; - } - $conditions = array(); - if ($isThreeDimensional!=NULL) { - $conditions['isThreeDimensional'] = $isThreeDimensionalValue; - } - if ($isTimeSeries!=NULL) { - $conditions['isTimeSeries'] = $isTimeSeriesValue; - } - return $this->retrieveColumnFromTableWhere("name", "geometry", $conditions); - } - - /*! - \brief Return all values from the column from the table where the condition - evaluates to true - \param $column Name of the column from which the values are taken - \param $table Name of the table from which the values are taken - \param $conditions Array of conditions that the result values must fullfil. - This is an array with column names as indices and - boolean values as content. - \return array of values - */ - public function retrieveColumnFromTableWhere($column, $table, $conditions) { - $query = "select distinct $column from $table where "; - foreach ($conditions as $eachName => $eachValue) { - $query = $query . $eachName . " = '" . $eachValue . "' and "; - } - $query = $query . "1 = 1"; - $answer = $this->query($query); - $result = array(); - - if ( !empty($answer) ) { - foreach ($answer as $row) { - $result[] = end($row); - } - } - - return $result; - } - - /*! - \brief Returns the default value for a given parameter - \param $parameterName Name of the parameter - \return Default value - */ - public function defaultValue($parameterName) { - $query = "SELECT value FROM possible_values WHERE parameter='"; - $query .= $parameterName . "' AND isDefault='t'"; - $result = $this->queryLastValue($query); - if ($result === False) { - return NULL; - } - - return $result; - } - - /*! - \brief Returns the id for next job from the queue, sorted by priority - \return Job id - */ - public function getNextIdFromQueue() { - // For the query we join job_queue and job_files, since we want to sort also by file name - $query = "SELECT id - FROM job_queue, job_files - WHERE job_queue.id = job_files.job AND job_queue.username = job_files.owner - AND job_queue.status = 'queued' - ORDER BY job_queue.priority desc, job_queue.status desc, job_files.file desc"; - $result = $this->queryLastValue($query); - if (!$result) { - return NULL; - } - return $result; - } - - /*! - \brief Returns all jobs from the queue, both compound and simple, - ordered by priority - \return all jobs - */ - public function getQueueJobs() { - // Get jobs as they are in the queue, compound or not, without splitting - // them. - $query = "SELECT id, username, queued, start, server, process_info, status - FROM job_queue - ORDER BY job_queue.priority asc, job_queue.queued asc, job_queue.status asc"; - $result = $this->query($query); - return $result; - } - - /*! - \brief Returns all jobs from the queue, both compund and simple, - and the associated file names, ordered by priority - \return all jobs - */ - public function getQueueContents() { - // For the query we join job_queue and job_files, since we want to sort also by file name - $query = "SELECT id, username, queued, start, stop, server, process_info, status, file - FROM job_queue, job_files - WHERE job_queue.id = job_files.job AND job_queue.username = job_files.owner - ORDER BY job_queue.priority asc, job_queue.queued asc, job_queue.status asc, job_files.file asc - LIMIT 100"; - $result = $this->query($query); - return $result; - } - - /*! - \brief Returns all jobs from the queue for a given id (that must be - univocal!) - \param $id String Id of the job - \return all jobs for the id - */ - public function getQueueContentsForId($id) { - $query = "select id, username, queued, start, server, process_info, status from job_queue where id='" . $id . "'"; - $result = $this->queryLastRow($query); // it is supposed that just one job exists with a given id - return $result; - } - - /*! - \brief Returns all file names associated to a job with given id - \param $id Job id - \return array of file names - */ - public function getJobFilesFor($id) { - $query = "select file from job_files where job = '" . $id . "'"; - $result = $this->query($query); - $result = $this->flatten($result); - return $result; - } - - /*! - \brief Returns the file series mode of a job with given id - \param $id Job id - \return true or false - */ - public function getSeriesModeForId($id) { - $query = "select autoseries from job_files where job = '" . $id . "'"; - $result = $this->queryLastValue($query); - - return $result; - } - - /*! - \brief Returns the number of jobs currently in the queue for a - given username - \param $username Name of the user - \return number of jobs in queue - */ - public function getNumberOfQueuedJobsForUser($username) { - $query = "SELECT COUNT(id) FROM job_queue WHERE username = '" . $username . "';"; - $row = $this->Execute( $query )->FetchRow( ); - return $row[ 0 ]; - } - - /*! - \brief Returns the total number of jobs currently in the queue - \return total number of jobs in queue - */ - public function getTotalNumberOfQueuedJobs() { - $query = "SELECT COUNT(id) FROM job_queue;"; - $row = $this->Execute( $query )->FetchRow( ); - return $row[ 0 ]; - } - - /*! - \brief Returns the name of the user who created the job with given id - \param $id String id of the job - \return name of the user - */ - public function userWhoCreatedJob($id) { - $query = "select username from job_queue where id = '" . $id . "'"; - $result = $this->queryLastValue($query); - if (!$result) { - return NULL; - } - return $result; - } - - /*! - \brief Deletes job with specified ID from all job tables - \param $id Id of the job - \return true if success - */ - public function deleteJobFromTables($id) { - // TODO: Use foreign keys in the database! - $result = True; - $result = $result && $this->execute( - "delete from job_analysis_parameter where setting='$id'"); - $result = $result && $this->execute( - "delete from job_analysis_setting where name='$id'"); - $result = $result && $this->execute( - "delete from job_files where job='$id'"); - $result = $result && $this->execute( - "delete from job_parameter where setting='$id'"); - $result = $result && $this->execute( - "delete from job_parameter_setting where name='$id'"); - $result = $result && $this->execute( - "delete from job_queue where id='$id'"); - $result = $result && $this->execute( - "delete from job_task_parameter where setting='$id'"); - $result = $result && $this->execute( - "delete from job_task_setting where name='$id'"); - return $result; - } - - /*! - \brief Returns the path to hucore on given host - \param $host String Host name - \return full path to hucore - */ - // TODO better management of multiple hosts - function huscriptPathOn($host) { - $query = "SELECT huscript_path FROM server where name = '" . $host . "'"; - $result = $this->queryLastValue($query); - if (!$result) { - return NULL; - } - return $result; - } - - /*! - \brief Get the name of a free server - \return name of the server - */ - public function freeServer() { - $query = "select name from server where status='free'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Get the status (i.e. free, busy, paused) of server $name - \param $name Name of the server - \return status - */ - public function statusOfServer($name) { - $query = "select status from server where name='$name'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Checks whether server is busy - \param $name Name of the server - \return true if the server is busy, false otherwise - */ - public function isServerBusy($name) { - $status = $this->statusOfServer($name); - $result = ($status == 'busy'); - return $result; - } - - /*! - \brief Checks whether the switch in the queue manager is 'on' - \return true if on - */ - public function isSwitchOn() { - // Handle some back-compatibility issue - if ($this->doGlobalVariablesExist()) { - $query = "SELECT value FROM queuemanager WHERE field = 'switch'"; - $answer = $this->queryLastValue($query); - $result = True; - if ($answer == 'off') { - $result = False; - report("$query; returned '$answer'", 1); - notifyRuntimeError("hrmd stopped", - "$query; returned '$answer'\n\nThe HRM queue manage will stop."); - } - } - else { - $query = "select switch from queuemanager"; - $answer = $this->queryLastValue($query); - $result = True; - if ($answer == 'off') { - $result = False; - report("$query; returned '$answer'", 1); - notifyRuntimeError("hrmd stopped", - "$query; returned '$answer'\n\nThe HRM queue manage will stop."); - } - } - - return $result; - } - - /*! - \brief Gets the status of the queue manager's switch - \return 'on' or 'off' - */ - public function getSwitchStatus() { - if ($this->doGlobalVariablesExist()) { - $query = "SELECT value FROM queuemanager WHERE field = 'switch'"; - $answer = $this->queryLastValue($query); - } - else { - $query = "select switch from queuemanager"; - $answer = $this->queryLastValue($query); - } - return $answer; - } - - /*! - \brief Sets the status of the queue manager's switch - \param $status String Either 'on' or 'off' - \return query result - */ - public function setSwitchStatus( $status ) { - $result = $this->execute("UPDATE queuemanager SET value = '". $status ."' WHERE field = 'switch'"); - return $result; - } - - /*! - \brief Sets the state of the server to 'busy' and the pid for a running job - \param $name String Server name - \param $pid String Process identifier associated with a running job - \return query result - */ - public function reserveServer($name, $pid) { - $query = "update server set status='busy', job='$pid' where name='$name'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Sets the state of the server to 'free' and deletes the the pid - \param $name String Server name - \param $pid Process identifier associated with a running job (UNUSED!) - \return query result - */ - public function resetServer($name, $pid) { - $query = "update server set status='free', job=NULL where name='$name'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Starts a job - \param $job Job object - \return query result - */ - public function startJob($job) { - $desc = $job->description(); - $id = $desc->id(); - $server = $job->server(); - $process_info = $job->pid(); - $query = "update job_queue set start=NOW(), server='$server', process_info='$process_info', status='started' where id='" .$id . "'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Get all running jobs - \return array of Job objects - */ - public function getRunningJobs() { - $result = array(); - $query = "select id, process_info, server from job_queue where status = 'started'"; - $rows = $this->query($query); - if (!$rows) return $result; - - foreach ($rows as $row) { - $desc = new JobDescription(); - $desc->setId($row['id']); - $desc->load(); - $job = new Job($desc); - $job->setServer($row['server']); - $job->setPid($row['process_info']); - $job->setStatus('started'); - $result[] = $job; - } - return $result; - } - - /*! - \brief Get names of all processing servers (independent of their status) - \return array of strings - */ - public function availableServer() { - $query = "select name from server"; - $result = $this->query($query); - $result = $this->flatten($result); - return $result; - } - - /*/! - \brief Get the starting time of given job object - \param $job Job object - \return Start time (String) - */ - public function startTimeOf( Job $job ) { - $desc = $job->description(); - $id = $desc->id(); - $query = "select start from job_queue where id = '" .$id . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*!--------------------------------------------------------- - \brief Returns a formatted time from a unix timestamp - \param $timestamp Unix timestamp - \return formatted time string: YYYY-MM-DD hh:mm:ss - */ - public function fromUnixTime($timestamp) { - $query = "select FROM_UNIXTIME($timestamp)"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Pauses a job of given id - \param $id Job id - \return query result - */ - public function pauseJob($id) { - $query = "update job_queue set status='paused' where id='" . $id . "'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Sets the end time of a job - \param $id Job id - \param $date Formatted date: YYYY-MM-DD hh:mm:ss - \return query result - */ - public function setJobEndTime($id, $date) { - $query = "update job_queue set stop='".$date."' where id='" . $id . "'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Changes status of 'paused' jobs to 'queued' - \return query result - */ - public function restartPausedJobs() { - $query = "update job_queue set status='queued' where status='paused'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Marks a job with given id as 'broken' (i.e. to be removed) - \param $id Job id - \return query result - */ - public function markJobAsRemoved($id) { - $query = "update job_queue set status='broken' where (status='queued' or status='paused') and id='" . $id . "'"; - // $query = "update job_queue set status='broken' where id='" . $id . "'"; - $result = $this->execute($query); - $query = "update job_queue set status='kill' where status='started' and id='" . $id . "'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Set the server status to free - \param $server Server name - \return query result - */ - public function markServerAsFree($server) { - $query = "update server set status='free', job=NULL where name='" . $server . "'"; - $result = $this->execute($query); - return $result; - } - - /*! - \brief Get all jobs with status 'broken' - \return array of ids - */ - public function getMarkedJobIds() { - $conditions['status'] = 'broken'; - $ids = $this->retrieveColumnFromTableWhere('id', 'job_queue', $conditions); - return $ids; - } - - /*! - \brief Get all jobs with status 'kill' to be killed by the Queue Manager - \return array of ids - */ - public function getJobIdsToKill() { - $conditions['status'] = 'kill'; - $ids = $this->retrieveColumnFromTableWhere('id', 'job_queue', $conditions); - return $ids; - } - - /*! - \brief Check whether a user exists - \param $name Name of the user - \return true if the user exists - */ - public function checkUser($name) { - $query = "select status from username where name = '" . $name . "'"; - $result = $this->queryLastValue($query); - if ($result) $result = true; - return $result; - } - - /*! - \brief Get the status of a user - \param $name Name of the user - \return status ('a', 'd', ...) - */ - public function getUserStatus($name) { - $query = "select status from username where name = '" . $name . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Return the list of known users. - \param String User name to filter out from the list (optional). - \return Array of users. - */ - public function getUserList($name) { - $query = "select name from username where name != '" . $name . "' " . - " and name != 'admin';"; - $result = $this->query($query); - return $result; - } - - /*! - \brief Get the name of the user who owns a job with given id - \param $id Job id - \return name of the user who owns the job - */ - public function getJobOwner($id) { - $query = "select username from job_queue where id = '" . $id . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Returns current date and time - \return formatted date (YYYY-MM-DD hh:mm:ss) - */ - public function now() { - $query = "select now()"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Returns the group to which the user belongs - \param $userName Name of the user - \return group name - */ - public function getGroup($userName) { - $query = "SELECT research_group FROM username WHERE name= '" . $userName . "'"; - $result = $this->queryLastValue($query); - return $result; - } - - /*! - \brief Updates the e-mail address of a user - \param $userName Name of the user - \param $email E-mail address - \return query result - */ - public function updateMail($userName, $email) { - $cmd = "UPDATE username SET email = '". $email ."' WHERE name = '".$userName."'"; - $result = $this->execute($cmd); - return $result; - } - - /*! - \brief Updates the last access date of a user - \param $userName Name of the user - \return query result - */ - public function updateLastAccessDate($userName) { - $query = "UPDATE username SET last_access_date = CURRENT_TIMESTAMP WHERE name = '". $userName . "'"; - $result = $this->execute($query); - return $result; - } - - - /*! - \brief Gets the maximum number of channels from the database. - \return The number of channels. - */ - public function getMaxChanCnt() { - $query = "SELECT MAX(value) as \"\" FROM possible_values "; - $query .= "WHERE parameter='NumberOfChannels'"; - $result = trim($this->execute($query)); - - if (!is_numeric($result)) { - $result = 5; - } - - return $result; - } - - - /*! - \brief Get the list of Setting's for the User - - The Parameter values are not loaded. - - \return array of Setting's - */ - public function getSettingList( $username, $table ) { - $query = "select name, standard from $table where owner ='" . $username . "' order by name"; - return ( $this->query( $query ) ); - } - - /*! - \brief Get the parameter confidence level for given file format - \param $fileFormat File format for which the Parameter confidence level is queried - (not strictly necessary for the Parameters with confidence level 'Provide', - could be set to '' for those) - \param $parameterName Name of the Paramater the confidence level should be returned - \return parameter confidence level - */ - public function getParameterConfidenceLevel( $fileFormat, $parameterName ) { - // Some Parameters MUST be provided by the user and cannot be overridden - // by the file metadata - switch ( $parameterName ) { - case 'ImageFileFormat' : - case 'NumberOfChannels' : - case 'PointSpreadFunction': - case 'MicroscopeType' : - case 'CoverslipRelativePosition': - case 'PerformAberrationCorrection': - case 'AberrationCorrectionMode': - case 'AdvancedCorrectionOptions': - case 'PSF' : - return "provided"; - case 'Binning': - case 'IsMultiChannel': - case 'ObjectiveMagnification': - case 'CMount': - case 'TubeFactor': - case 'AberrationCorrectionNecessary': - case 'CCDCaptorSize': - case 'PSFGenerationDepth': - return "default"; - default: - - // For the other Parameters, the $fileFormat must be specified - if ( ( $fileFormat == '' ) && ( $fileFormat == null ) ) { - exit( "Error: please specify a file format!" . "\n" ); - } - - // The wavelength and voxel size parameters have a common confidence in - // the HRM but two independent confidences in hucore - if ( ( $parameterName == "ExcitationWavelength" ) || - ( $parameterName == "EmissionWavelength" ) ) { - - $confidenceLevelEx = $this->huCoreConfidenceLevel( - $fileFormat, "ExcitationWavelength" ); - $confidenceLevelEm = $this->huCoreConfidenceLevel( - $fileFormat, "EmissionWavelength" ); - $confidenceLevel = $this->minConfidenceLevel( - $confidenceLevelEx, $confidenceLevelEm ); - - } elseif ( ( $parameterName == "CCDCaptorSizeX" ) || - ( $parameterName == "ZStepSize" ) ) { - - $confidenceLevelX = $this->huCoreConfidenceLevel( - $fileFormat, "CCDCaptorSizeX" ); - $confidenceLevelZ = $this->huCoreConfidenceLevel( - $fileFormat, "ZStepSize" ); - $confidenceLevel = $this->minConfidenceLevel( - $confidenceLevelX, $confidenceLevelZ ); - - } else { - - $confidenceLevel = $this->huCoreConfidenceLevel( - $fileFormat, $parameterName ); - - } - - // Return the confidence level - return $confidenceLevel; - - } - - } - - /*! - \brief Finds out whether a Huygens module is supported by the license. - \param $feature The module to find out about. It can use (SQL) wildcards. - \return Boolean: true if the module is supported by the license. - */ - public function hasLicense ( $feature ) { - - $query = "SELECT feature FROM hucore_license WHERE " . - "feature LIKE '" . $feature . "' LIMIT 1;"; - - if ( $this->queryLastValue($query) === FALSE ) { - return false; - } else { - return true; - } - } - - /*! - \brief Checks whether Huygens Core has a valid license - \return true if the license is valid, false otherwise - */ - public function hucoreHasValidLicense( ) { - - // We (ab)use the hasLicense() method - return ( $this->hasLicense("freeware") == false); - } - - /*! - \brief Gets the licensed server type for Huygens Core. - \return one of desktop, small, medium, large, extreme - */ - public function hucoreServerType() { - - $query = "SELECT feature FROM hucore_license WHERE feature LIKE 'server=%';"; - $server = $this->queryLastValue($query); - if ($server == false) { - return "no server information"; - } - return substr($server, 7); - } - - /*! - \brief Updates the database with the current HuCore license details. - \param $licDetails A string with the supported license features. - \return Boolean: true if the license details were successfully saved. - */ - public function storeLicenseDetails ( $licDetails ) { - - $licStored = true; - - // Make sure that the hucore_license table exists. - $tables = $this->connection->MetaTables("TABLES"); - if (!in_array("hucore_license", $tables) ) { - $msg = "Table hucore_license does not exist! " . - "Please update the database!"; - report( $msg, 1 ); exit( $msg ); - } - - // Empty table: remove existing values from older licenses. - $query = "DELETE FROM hucore_license"; - $result = $this->execute($query); - - if (!$result) { - report("Could not store license details in the database!\n", 1); - $licStored = false; - return $licStored; - } - - // Populate the table with the new license. - $features = explode(" ", $licDetails); - foreach ($features as $feature) { - - switch( $feature ) { - case 'desktop': - case 'small': - case 'medium': - case 'large': - case 'extreme': - $feature = "server=" . $feature; - report("Licensed server: $feature", 1); - default: - report("Licensed feature: $feature", 1); - } - - $query = "INSERT INTO hucore_license (feature) ". - "VALUES ('". $feature ."')"; - $result = $this->execute($query); - - if (!$result) { - report("Could not store license feature - '$feature' in the database!\n", 1); - $licStored = false; - break; - } - } - - return $licStored; - } - - /*! - \brief Store the confidence levels returned by huCore into the database for faster retrieval - - This is a rather low-level function that creates the table if needed. - - \param $confidenceLevels Array of confidence levels with file formats as keys - \return true if storing (or updating) the database was successful, false otherwise - */ - public function storeConfidenceLevels( $confidenceLevels ) { - - // Make sure that the confidence_levels table exists - $tables = $this->connection->MetaTables("TABLES"); - if (!in_array("confidence_levels", $tables) ) { - $msg = "Table confidence_levels does not exist! " . - "Please update the database!"; - report( $msg, 1 ); exit( $msg ); - } - - // Get the file formats - $fileFormats = array_keys( $confidenceLevels ); - - // Get the keys of the parameter arrays from the first array (the others are the same) - $parameters = array_keys( $confidenceLevels[ $fileFormats[ 0 ] ] ); - - // Go over all $confidenceLevels and set the values - foreach ( $fileFormats as $format ) { - - // If the row for current $fileFormat does not exist, INSERT a new - // row with all parameters, otherwise UPDATE the existing one. - $query = "SELECT fileFormat FROM confidence_levels WHERE " . - "fileFormat = '" . $format . "' LIMIT 1;"; - - if ( $this->queryLastValue($query) === FALSE ) { - - // INSERT - if ( !$this->connection->AutoExecute( "confidence_levels", - $confidenceLevels[ $format ], "INSERT" ) ) { - $msg = "Could not insert confidence levels for file format $format!"; - report( $msg, 1 ); exit( $msg ); - } - - } else { - - // UPDATE - if ( !$this->connection->AutoExecute( "confidence_levels", - $confidenceLevels[ $format ], 'UPDATE', - "fileFormat = '$format'" ) ) { - $msg = "Could not update confidence levels for file format $format!"; - report( $msg, 1 ); exit( $msg); - } - - } - - } - - return true; - - } - - /*! - \brief Checks whether a user with a given seed exists in the database - - If a user requests an account, his username is added to the database with - a random seed as status. - - \return true if a user with given seed exists, false otherwise - */ - public function existsUserRequestWithSeed($seed) { - $query = "SELECT status FROM username WHERE status = '" . $seed . "'"; - $value = $this->queryLastValue($query); - if ($value == false) { - return false; - } else { - return ( $value == $seed ); - } - - } - - - public function switchGPUState( $newState ) { - if ( $newState == "On" ) { - $value = TRUE; - } else if ( $newState == "Off" ) { - $value = FALSE; - } else { - return "Impossible to change the GPU configuration. Unknown value."; - } - - $query = "UPDATE global_variables SET value = '". $value ."' " . - "WHERE name = 'GPUenabled';"; - - $result = $this->execute($query); - if ( $result ) { - return "GPU processing has been turned " . - strtolower($newState) . "."; - } else { - return "Impossible to change the GPU configuration."; - } - } - - public function getGPUStateAsString( ) { - $query = "SELECT value FROM global_variables " . - "WHERE name = 'GPUenabled';"; - - if ($this->queryLastValue($query)) { - return "true"; - } else { - return "false"; - } - } - - - /* - PRIVATE FUNCTIONS - */ - - /*! - \brief Return the mapped HuCore file format corresponding to HRM's - \param $fileFormat HRM's file format - \return mapped HuCore file format - */ - private function minConfidenceLevel( $level1, $level2 ) { - $levels = array( ); - $levels[ 'default' ] = 0; - $levels[ 'estimated' ] = 1; - $levels[ 'reported' ] = 2; - $levels[ 'verified' ] = 3; - $levels[ 'asIs' ] = 3; - - if ( $levels[ $level1 ] <= $levels[ $level2 ] ) { - return $level1; - } else { - return $level2; - } - - } - - /*! - \brief Return the raw HuCore confidence level - \param $fileFormat HRM's file format - \param $parameterName Name of the HRM Paramater - \return HuCore's raw confidence level - */ - private function huCoreConfidenceLevel( $fileFormat, $parameterName ) { - - // Get the mapped file format - $query = "SELECT hucoreName FROM file_format WHERE name = '" . - $fileFormat . "' LIMIT 1"; - $hucoreFileFormat = $this->queryLastValue( $query ); - if ( !$hucoreFileFormat ) { - report( "Could not get the mapped file name for " . $fileFormat ."!", 1 ); - return "default"; - } - - // Use the mapped file format to retrieve the - if (!array_key_exists($parameterName, $this->parameterNameDictionary)) { - return "default"; - } - $query = "SELECT " . $this->parameterNameDictionary[ $parameterName ] . - " FROM confidence_levels WHERE fileFormat = '" . $hucoreFileFormat . - "' LIMIT 1;"; - $confidenceLevel = $this->queryLastValue( $query ); - if ( !$confidenceLevel ) { - report( "Could not get the confidence level for " . $fileFormat ."!", 1 ); - return "default"; - } - - // return the confidence level - return $confidenceLevel; - } - - /*! - \brief Ugly hack to check for old table structure - \return true if global variables exist - */ - private function doGlobalVariablesExist() { - global $db_type; - global $db_host; - global $db_name; - global $db_user; - global $db_password; - - $test = False; - - $dsn = $db_type."://".$db_user.":".$db_password."@".$db_host."/".$db_name; - $db = ADONewConnection($dsn); - if(!$db) - return; - $tables = $db->MetaTables("TABLES"); - if (in_array("global_variables", $tables)) - $test = True; - - return $test; - } - -} -?> diff --git a/inc/Database.php b/inc/Database.php new file mode 100644 index 000000000..912f48225 --- /dev/null +++ b/inc/Database.php @@ -0,0 +1,2363 @@ +connection = ADONewConnection($db_type); + $this->connection->Connect($db_host, $db_user, $db_password, $db_name); + + // Set the parameter name dictionary + $this->parameterNameDictionary = array( + "CCDCaptorSizeX" => "sampleSizesX", // In HRM there is no distinction between x and y pixel size + "ZStepSize" => "sampleSizesZ", + "TimeInterval" => "sampleSizesT", + "PinholeSize" => "pinhole", + "NumberOfChannels" => "chanCnt", + "PinholeSpacing" => "pinholeSpacing", + "ExcitationWavelength" => "lambdaEx", + "EmissionWavelength" => "lambdaEm", + "MicroscopeType" => "mType", + "NumericalAperture" => "NA", + "ObjectiveType" => "RILens", + "SampleMedium" => "RIMedia", + "unused1" => "iFacePrim", // PSFGenerationDepth? + "unused2" => "iFaceScnd", + "unused3" => "imagingDir", + "unused4" => "objQuality", + "unused5" => "photonCnt", + "unused6" => "exBeamFill", + "StedDepletionMode" => "stedMode", + "StedWavelength" => "stedLambda", + "StedSaturationFactor" => "stedSatFact", + "StedImmunity" => "stedImmunity", + "Sted3D" => "sted3D"); + } + + /** + * Checks whether a connection to the DB is possible. + * @return boolean True if the connection is possible, false otherwise. + */ + public function isReachable() + { + global $db_type; + global $db_host; + global $db_name; + global $db_user; + global $db_password; + /** @var ADODB_mysql|\ADODB_postgres8|\ADODB_postgres9 $connection */ + $connection = ADONewConnection($db_type); + $result = $connection->Connect($db_host, $db_user, $db_password, $db_name); + return $result; + } + + /** + * Returns the type of the database (mysql, postgres) + * @return string The type of the database (e.g. mysql, postgres) + */ + public function type() + { + global $db_type; + return $db_type; + } + + /** + * Attempts to get the version of the underlying database. + * @return string Version of the database (e.g. 2.2.14). + */ + public function version() + { + try { + $query = "SELECT version( );"; + $version = $this->queryLastValue($query); + } catch (\Exception $e) { + $version = "Could not get version information."; + } + return $version; + } + + /** + * Returns the database host name. + * @return string Name of the database host. + */ + public function host() + { + global $db_host; + return $db_host; + } + + /** + * Returns the database name. + * @return string Name of the database. + */ + public function name() + { + global $db_name; + return $db_name; + } + + /** + * Returns the name of the database user. + * @return string Name of the database user. + */ + public function user() + { + global $db_user; + return $db_user; + } + + /** + * Returns the password of the database user. + * @return string Password of the database user. + */ + public function password() + { + global $db_password; + return $db_password; + } + + /** + * Returns the ADOConnection object. + * @return \ADORecordSet_mysql|\ADODB_postgres8|\ADODB_postgres9 The connection object. + */ + public function connection() + { + return $this->connection; + } + + /** + * Executes an SQL query. + * @param string $query SQL query. + * @return \ADORecordSet_empty|\ADORecordSet_mysql|False Query result. + */ + public function execute($query) + { + /** @var ADODB_mysql|\ADODB_postgres8|\ADODB_postgres9 $connection */ + $connection = $this->connection(); + $result = $connection->Execute($query); + return $result; + } + + /** + * Executes an SQL query and returns the results. + * @param string $queryString SQL query. + * @return array|false Result of the query (rows). + */ + public function query($queryString) + { + $resultSet = $this->connection()->Execute($queryString); + if ($resultSet === false) { + return False; + } + /** @var \ADORecordSet $resultSet */ + $rows = $resultSet->GetRows(); + return $rows; + } + + /** + * Executes an SQL query and returns the results. + * @param string $sql Prepared SQL query. + * @param array $values Array of values for the prepared query. + * @return array|false Result of the query (rows). + */ + public function queryPrepared($sql, array $values) + { + $resultSet = $this->connection()->Execute($sql, $values); + if ($resultSet === false) { + return False; + } + /** @var \ADORecordSet $resultSet */ + $rows = $resultSet->GetRows(); + return $rows; + } + + /** + * Executes an SQL query and returns the last row of the results. + * @param string $queryString SQL query. + * @return array Last row of the result of the query. + */ + public function queryLastRow($queryString) + { + $rows = $this->query($queryString); + if (!$rows) { + return False; + } + $result = end($rows); + return $result; + } + + /** + * Executes an SQL query and returns the value in the last column of the + * last row of the results. + * @param string $queryString SQL query. + * @return string Value of the last column of the last row of the result of + * the query. + */ + public function queryLastValue($queryString) + { + $rows = $this->queryLastRow($queryString); + if (!$rows) { + return False; + } + $result = end($rows); + return $result; + } + + /** + * Saves the parameter values of the setting object into the database. + * + * If the setting already exists, the old values are overwritten, otherwise + * a new setting is created. + * + * @param \hrm\setting\base\Setting $settings Settings object to be saved. + * @return bool True if saving was successful, false otherwise. + */ + public function saveParameterSettings(Setting $settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + $settingTable = $settings->table(); + $table = $settings->parameterTable(); + if ($settings->isDefault()) + $standard = "t"; + else + $standard = "f"; + $result = True; + if (!$this->existsSetting($settings)) { + $query = "insert into $settingTable values ('$user', '$name'" . + ", '$standard')"; + $result = $result && $this->execute($query); + } + $existsAlready = $this->existsParametersFor($settings); + + foreach ($settings->parameterNames() as $parameterName) { + $parameter = $settings->parameter($parameterName); + $parameterValue = $parameter->internalValue(); + + if (is_array($parameterValue)) { + /*! Before, # was used as a separator, but the first element + with index zero was always NULL because channels started + their indexing at one. To keep backwards compatibility with + the database, we use # now as a channel marker, and even the + first channel has a # in front of its value "/" separator is + used to mark range values for signal to noise ratio. + */ + + /*! + @todo Currently there are not longer "range values" (values + separated by /). In the future they will be reintroduced. + We leave the code in place. + */ + if (is_array($parameterValue[0])) { + $maxChanCnt = $this->getMaxChanCnt(); + for ($i = 0; $i < $maxChanCnt; $i++) { + if ($parameterValue[$i] != null) { + $parameterValue[$i] = + implode("/", array_filter($parameterValue[$i])); + } + } + } + $parameterValue = "#" . implode("#", $parameterValue); + } + + if (!$existsAlready) { + $query = "INSERT INTO $table VALUES ('$user', '$name', " . + "'$parameterName', '$parameterValue');"; + } else { + /* Check that the parameter itself exists. */ + $query = "SELECT name FROM $table WHERE owner='$user' AND " . + "setting='$name' AND name='$parameterName' LIMIT 1;"; + $newValue = $this->queryLastValue($query); + + if ($newValue != NULL) { + $query = "UPDATE $table SET value = '$parameterValue' " . + "WHERE owner='$user' AND setting='$name' " . + "AND name='$parameterName';"; + } else { + $query = "INSERT INTO $table VALUES ('$user', '$name', " + . "'$parameterName', '$parameterValue');"; + } + } + + // Accumulate the successes (or failures) of the queries. If a query + // fails, the return of $this->execute() will be === false; otherwise + // it is an ADORecordSet. + $result &= ($this->execute($query) !== false); + } + + + return $result; + } + + /** + * Save the parameter values of the setting object into the shared tables. + * + * @param Setting $settings Settings object to be saved. + * @param string $targetUserName User name of the user that the Setting is + * to be shared with. + * @return bool True if saving was successful, false otherwise. + */ + public function saveSharedParameterSettings($settings, $targetUserName) + { + $owner = $settings->owner(); + $original_user = $owner->name(); + $name = $settings->name(); + $new_owner = new UserV2(); + $new_owner->setName($targetUserName); + $settings->setOwner($new_owner); + /** @var ParameterSetting|TaskSetting|AnalysisSetting $settings */ + $settingTable = $settings->sharedTable(); + $table = $settings->sharedParameterTable(); + $result = True; + if (!$this->existsSharedSetting($settings)) { + $query = "insert into $settingTable " . + "(owner, previous_owner, sharing_date, name) values " . + "('$targetUserName', '$original_user', CURRENT_TIMESTAMP, '$name')"; + $result = $result && $this->execute($query); + } + + if (!$result) { + return False; + } + + // Get the Id + $query = "select id from $settingTable where " . + "owner='$targetUserName' AND previous_owner='$original_user' " . + "AND name='$name'"; + $id = $this->queryLastValue($query); + if (!$id) { + return False; + } + + // Get the parameter names + $parameterNames = $settings->parameterNames(); + + // Add the parameters + foreach ($parameterNames as $parameterName) { + + $parameter = $settings->parameter($parameterName); + $parameterValue = $parameter->internalValue(); + + if (is_array($parameterValue)) { + // Before, # was used as a separator, but the first element with + // index zero was always NULL because channels started their indexing + // at one. To keep backwards compatibility with the database, we use + // # now as a channel marker, and even the first channel has a # in + // front of its value. + // "/" separator is used to mark range values for signal to noise ratio + + + // Special treatment for the PSF parameter. + if ($parameter->name() == "PSF") { + + // Create hard links and update paths to the PSF files + // to point to the hard-links. + $fileServer = new Fileserver($original_user); + $parameterValue = $fileServer->createHardLinksToSharedPSFs( + $parameterValue, $targetUserName); + + } + + /*! + @todo Currently there are not longer "range values" (values + separated by /). In the future they will be reintroduced. + We leave the code in place. + */ + if (is_array($parameterValue[0])) { + $maxChanCnt = $this->getMaxChanCnt(); + for ($i = 0; $i < $maxChanCnt; $i++) { + if ($parameterValue[$i] != null) { + $parameterValue[$i] = implode("/", array_filter($parameterValue[$i])); + } + } + } + $parameterValue = "#" . implode("#", $parameterValue); + } + + $query = "insert into $table " . + "(setting_id, owner, setting, name, value) " . + "values ('$id', '$targetUserName', '$name', " . + "'$parameterName', '$parameterValue');"; + $result = $result && $this->execute($query); + } + + return $result; + } + + /** + * Loads the parameter values for a setting and returns a copy of the + * setting with the loaded parameter values. + * + * If a value starts with # it is considered to be an array with the first + * value at the index 0. + * @param Setting $settings Setting object to be loaded. + * @return Setting $settings Setting object with loaded values. + * @todo Debug the switch blog (possibly buggy!) + */ + public function loadParameterSettings($settings) + { + $user = $settings->owner(); + $user = $user->name(); + $name = $settings->name(); + $table = $settings->parameterTable(); + + foreach ($settings->parameterNames() as $parameterName) { + $parameter = $settings->parameter($parameterName); + $query = "SELECT value FROM $table WHERE owner='$user' AND " . + "setting='$name' AND name='$parameterName';"; + + $newValue = $this->queryLastValue($query); + + if ($newValue == NULL) { + + // See if the Parameter has a usable default + $newValue = $parameter->defaultValue(); + if ($newValue == NULL) { + continue; + } + } + + + if ($newValue{0} == '#') { + switch ($parameterName) { + case "ExcitationWavelength": + case "EmissionWavelength": + case "PinholeSize": + case "PinholeSpacing": + case "SignalNoiseRatio": + case "BackgroundOffsetPercent": + case "ChromaticAberration": + case "StedDepletionMode": + case "StedWavelength": + case "StedSaturationFactor": + case "StedImmunity": + case "Sted3D": + case "SpimExcMode": + case "SpimGaussWidth": + case "SpimCenterOffset": + case "SpimFocusOffset": + case "SpimNA": + case "SpimFill": + case "SpimDir": + case "ColocChannel": + case "ColocThreshold": + case "ColocCoefficient": + /* Extract and continue to explode. */ + $newValue = substr($newValue, 1); + default: + $newValues = explode("#", $newValue); + } + + if (strcmp($parameterName, "PSF") != 0 + && strpos($newValue, "/") + ) { + $newValue = array(); + for ($i = 0; $i < count($newValues); $i++) { + if (strpos($newValues[$i], "/")) { + $newValue[] = explode("/", $newValues[$i]); + } else { + $newValue[] = array($newValues[$i]); + } + } + } else { + $newValue = $newValues; + } + } + + $parameter->setValue($newValue); + $settings->set($parameter); + } + + return $settings; + } + + /** + * Loads the parameter values for a setting fro mthe sharead tabled and + * returns it. + * @param int $id Setting id. + * @param string $type Setting type (one of "parameter", "task", "analysis"). + * @return Setting object with loaded values. + * @throws \Exception + * @todo Debug the second switch block (probably buggy!) + */ + public function loadSharedParameterSettings($id, $type) + { + + // Get the correct objects + switch ($type) { + + case "parameter": + + $settingTable = ParameterSetting::sharedTable(); + $table = ParameterSetting::sharedParameterTable(); + $settings = new ParameterSetting(); + break; + + case "task": + + $settingTable = TaskSetting::sharedTable(); + $table = TaskSetting::sharedParameterTable(); + $settings = new TaskSetting(); + break; + + case "analysis": + + $settingTable = AnalysisSetting::sharedTable(); + $table = AnalysisSetting::sharedParameterTable(); + $settings = new AnalysisSetting(); + break; + + default: + + throw new \Exception("bad value for type!"); + } + + // Get the setting info + $query = "select * from $settingTable where id=$id;"; + $response = $this->queryLastRow($query); + if (!$response) { + return NULL; + } + + // Fill the setting + $settings->setName($response["name"]); + $user = new UserV2(); + $user->setName($response["owner"]); + $settings->setOwner($user); + + // Load from shared table + foreach ($settings->parameterNames() as $parameterName) { + $parameter = $settings->parameter($parameterName); + $query = "select value from $table where setting_id=$id and name='$parameterName'"; + $newValue = $this->queryLastValue($query); + if ($newValue == NULL) { + // See if the Parameter has a usable default + $newValue = $parameter->defaultValue(); + if ($newValue == NULL) { + continue; + } + } + if ($newValue{0} == '#') { + switch ($parameterName) { + case "ExcitationWavelength": + case "EmissionWavelength": + case "SignalNoiseRatio": + case "BackgroundOffsetPercent": + case "ChromaticAberration": + /* Extract and continue to explode. */ + $newValue = substr($newValue, 1); + default: + $newValues = explode("#", $newValue); + } + + if (strcmp($parameterName, "PSF") != 0 && strpos($newValue, "/")) { + $newValue = array(); + for ($i = 0; $i < count($newValues); $i++) { + //$val = explode("/", $newValues[$i]); + //$range = array(NULL, NULL, NULL, NULL); + //for ($j = 0; $j < count($val); $j++) { + // $range[$j] = $val[$j]; + //} + //$newValue[] = $range; + /*! + @todo Currently there are not longer "range values" (values + separated by /). In the future they will be reintroduced. + We leave the code in place. + */ + if (strpos($newValues[$i], "/")) { + $newValue[] = explode("/", $newValues[$i]); + } else { + $newValue[] = array($newValues[$i]); + } + } + } else { + $newValue = $newValues; + } + } + //$shiftedNewValue = array(1 => NULL, 2 => NULL, 3 => NULL, 4 => NULL, 5 => NULL); + //if (is_array($newValue)) { + // // start array at 1 + // for ($i = 1; $i <= count($newValue); $i++) { + // $shiftedNewValue[$i] = $newValue[$i - 1]; + // } + //} + //else $shiftedNewValue = $newValue; + $parameter->setValue($newValue); + $settings->set($parameter); + } + return $settings; + } + + /** + * Returns the list of shared templates with the given user. + * @param string $username Name of the user for whom to query for shared + * templates. + * @param string $table Name of the shared table to query. + * @return array List of shared jobs. + */ + public function getTemplatesSharedWith($username, $table) + { + $query = "SELECT * FROM $table WHERE owner='$username'"; + $result = $this->query($query); + return $result; + } + + /** + * Returns the list of shared templates by the given user. + * @param string $username Name of the user for whom to query for shared + * templates. + * @param string $table Name of the shared table to query. + * @return array List of shared jobs. + */ + public function getTemplatesSharedBy($username, $table) + { + $query = "SELECT * FROM $table WHERE previous_owner='$username'"; + $result = $this->query($query); + return $result; + } + + /** + * Copies the relevant rows from shared- to user- tables. + * @param int $id ID of the setting to be copied. + * @param string $sourceSettingTable Setting table to copy from. + * @param string $sourceParameterTable Parameter table to copy from. + * @param string $destSettingTable Setting table to copy to. + * @param string $destParameterTable Parameter table to copy to. + * @return bool True if copying was successful; false otherwise. + */ + public function copySharedTemplate($id, $sourceSettingTable, + $sourceParameterTable, $destSettingTable, $destParameterTable) + { + + // Get the name of the previous owner (the one sharing the setting). + $query = "select previous_owner, owner, name from $sourceSettingTable where id=$id"; + $rows = $this->queryLastRow($query); + if (False === $rows) { + return False; + } + $previous_owner = $rows["previous_owner"]; + $owner = $rows["owner"]; + $setting_name = $rows["name"]; + + // Compose the new name of the setting + $out_setting_name = $previous_owner . "_" . $setting_name; + + // Check if a setting with this name already exists in the target tables + $query = "select name from $destSettingTable where " . + "name='$out_setting_name' and owner='$owner'"; + if ($this->queryLastValue($query)) { + + // The setting already exists; we try adding numerical indices + $n = 1; + $original_out_setting_name = $out_setting_name; + while (1) { + + $test_name = $original_out_setting_name . "_" . $n++; + $query = "select name from $destSettingTable where name='$test_name' and owner='$owner'"; + if (!$this->queryLastValue($query)) { + $out_setting_name = $test_name; + break; + } + } + + } + + // Get all rows from source table for given setting id + $query = "select * from $sourceParameterTable where setting_id=$id"; + $rows = $this->query($query); + if (count($rows) == 0) { + return False; + } + + // Now add the rows to the destination table + $ok = True; + $record = array(); + $this->connection->BeginTrans(); + foreach ($rows as $row) { + $record["owner"] = $row["owner"]; + $record["setting"] = $out_setting_name; + $record["name"] = $row["name"]; + + // PSF files must be processed differently + if ($record["name"] == "PSF") { + + // Instantiate a Fileserver object for the target user + $fileserver = new Fileserver($owner); + + // Get the array of PSF names + $values = $row["value"]; + if ($values[0] == "#") { + $values = substr($values, 1); + } + $psfFiles = explode('#', $values); + + // Create hard-links to the target user folder + $newPSFFiles = $fileserver->createHardLinksFromSharedPSFs( + $psfFiles, $owner, $previous_owner); + + // Update the entries for the database + $record["value"] = "#" . implode('#', $newPSFFiles); + + } else { + + $record["value"] = $row["value"]; + + } + + $insertSQL = $this->connection->GetInsertSQL($destParameterTable, + $record); + $status = $this->connection->Execute($insertSQL); + $ok &= !(false === $status); + if (!$ok) { + break; + } + } + + // If everything went okay, we commit the transaction; otherwise we roll + // back + if ($ok) { + $this->connection->CommitTrans(); + } else { + $this->connection->RollbackTrans(); + return False; + } + + // Now add the setting to the setting table + $query = "select * from $sourceSettingTable where id=$id"; + $rows = $this->query($query); + if (count($rows) != 1) { + return False; + } + + $ok = True; + $this->connection->BeginTrans(); + $record = array(); + $row = $rows[0]; + $record["owner"] = $row["owner"]; + $record["name"] = $out_setting_name; + $record["standard"] = 'f'; + $insertSQL = $this->connection->GetInsertSQL($destSettingTable, + $record); + $status = $this->connection->Execute($insertSQL); + $ok &= !(false === $status); + + if ($ok) { + $this->connection->CommitTrans(); + } else { + $this->connection->RollbackTrans(); + return False; + } + + // Now we can delete the records from the source tables. Even if it + // if it fails we do not roll back, since the parameters were copied + // successfully. + + // Delete setting entry + $query = "delete from $sourceSettingTable where id=$id"; + $status = $this->connection->Execute($query); + if (false === $status) { + return False; + } + + // Delete parameter entries + $query = "delete from $sourceParameterTable where setting_id=$id"; + $status = $this->connection->Execute($query); + if (false === $status) { + return False; + } + + return True; + } + + /** + * Delete the relevant rows from the shared tables. + * @param int $id ID of the setting to be deleted. + * @param string $sourceSettingTable Setting table to copy from. + * @param string $sourceParameterTable Parameter table to copy from. + * @return bool True if deleting was successful; false otherwise. + */ + public function deleteSharedTemplate($id, $sourceSettingTable, + $sourceParameterTable) + { + + // Initialize success + $ok = True; + + // Delete shared PSF files if any exist + if ($sourceParameterTable == "shared_parameter") { + $query = "select value from $sourceParameterTable where setting_id=$id and name='PSF'"; + $psfFiles = $this->queryLastValue($query); + if (NULL != $psfFiles && $psfFiles != "#####") { + if ($psfFiles[0] == "#") { + $psfFiles = substr($psfFiles, 1); + } + + // Extract PSF file paths from the string + $psfFiles = explode("#", $psfFiles); + + // Delete them + Fileserver::deleteSharedFSPFilesFromBuffer($psfFiles); + } + } + + // Delete setting entry + $query = "delete from $sourceSettingTable where id=$id"; + $status = $this->connection->Execute($query); + $ok &= !(false === $status); + + // Delete parameter entries + $query = "delete from $sourceParameterTable where setting_id=$id"; + $status = $this->connection->Execute($query); + $ok &= !(false === $status); + + return $ok; + } + + /** + * Updates the default entry in the database according to the default + * value in the setting. + * @param Setting $settings Settings object to be used to update the default. + * @return array query result. + */ + public function updateDefault($settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + if ($settings->isDefault()) + $standard = "t"; + else + $standard = "f"; + $table = $settings->table(); + $query = "update $table set standard = '$standard' where owner='$user' and name='$name'"; + $result = $this->execute($query); + return $result; + } + + /** + * Deletes the setting and all its parameter values from the database. + * @param Setting $settings Settings object to be used to delete all entries + * from the database. + * @return bool true if the setting and all parameters were deleted from the + * database; false otherwise. + */ + public function deleteSetting($settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + $result = True; + $table = $settings->parameterTable(); + $query = "delete from $table where owner='$user' and setting='$name'"; + $result = $result && $this->execute($query); + if (!$result) { + return FALSE; + } + $table = $settings->table(); + $query = "delete from $table where owner='$user' and name='$name'"; + $result = $result && $this->execute($query); + return $result; + } + + /** + * Checks whether parameters are already stored for a given setting. + * @param Setting $settings Settings object to be used to check for + * existence in the database. + * @return bool True if the parameters exist in the database; false otherwise. + */ + public function existsParametersFor($settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + $table = $settings->parameterTable(); + $query = "select name from $table where owner='$user' and setting='$name' LIMIT 1"; + $result = True; + if (!$this->queryLastValue($query)) { + $result = False; + } + return $result; + } + + /** + * Checks whether parameters are already stored for a given shared setting. + * @param ParameterSetting|TaskSetting|AnalysisSetting $settings Settings object to be used to check for existence + * in the database. + * @return bool True if the parameters exist in the database; false otherwise. + */ + public function existsSharedParametersFor($settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + $table = $settings->sharedParameterTable(); + $query = "select name from $table where owner='$user' and setting='$name' LIMIT 1"; + $result = True; + if (!$this->queryLastValue($query)) { + $result = False; + } + return $result; + } + + /** + * Checks whether settings exist in the database for a given owner. + * + * @param Setting $settings Settings object to be used to check for + * existence in the database (the name of the owner must be set in the + * settings). + * @return bool True if the settings exist in the database; false otherwise. + */ + public function existsSetting(Setting $settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + $table = $settings->table(); + $query = "select standard from $table where owner='$user' and name='$name' LIMIT 1"; + $result = True; + if (!$this->queryLastValue($query)) { + $result = False; + } + return $result; + } + + /** + * Checks whether shared settings exist in the database for a given owner. + * @param ParameterSetting|TaskSetting|AnalysisSetting $settings $settings Settings object to be used to check for + * existence in the database (the name of the owner must be set in the + * settings) + * @return bool True if the settings exist in the database; false otherwise. + */ + public function existsSharedSetting($settings) + { + $owner = $settings->owner(); + $user = $owner->name(); + $name = $settings->name(); + $table = $settings->sharedTable(); + $query = "select standard from $table where owner='$user' and name='$name' LIMIT 1"; + $result = True; + if (!$this->queryLastValue($query)) { + $result = False; + } + return $result; + } + + /** + * Adds all files for a given job id and user to the database. + * @param string $id Job id. + * @param string $owner Name of the user that owns the job. + * @param array $files Array of file names. + * @param bool $autoseries True if the series is to be loaded automatically, false otherwise. + * @return bool True if the job files could be saved successfully; false + * otherwise. + */ + public function saveJobFiles($id, $owner, $files, $autoseries) + { + $result = True; + /** @var UserV2 $owner */ + $username = $owner->name(); + $sqlAutoSeries = ""; + foreach ($files as $file) { + if (strcasecmp($autoseries, "TRUE") == 0 || strcasecmp($autoseries, "T") == 0) { + $sqlAutoSeries = "T"; + } + $slashesFile = addslashes($file); + $query = "insert into job_files values ('$id', '$username', '$slashesFile', '$sqlAutoSeries')"; + $result = $result && $this->execute($query); + } + return $result; + } + + /** + * Adds a job for a given job id and user to the queue. + * @param string $id Job id. + * @param string $username Name of the user that owns the job. + * @return array Query result. + */ + public function queueJob($id, $username) + { + $query = "insert into job_queue (id, username, queued, status) values ('$id', '$username', NOW(), 'queued')"; + return $this->execute($query); + } + + /** + * Assigns priorities to the jobs in the queue. + * @return True if assigning priorities was successful. + */ + public function setJobPriorities() + { + + $result = True; + + //////////////////////////////////////////////////////////////////////////// + // + // First we analyze the queue + // + //////////////////////////////////////////////////////////////////////////// + + // Get the number of users that currently have jobs in the queue + $users = $this->execute("SELECT DISTINCT( username ) FROM job_queue;"); + $row = $this->execute("SELECT COUNT( DISTINCT( username ) ) FROM job_queue;")->FetchRow(); + $numUsers = $row[0]; + + // 'Highest' priority (i.e. lowest value) is 0 + $currentPriority = 0; + + // First, we make sure to give the highest priorities to paused and + // broken jobs + $rs = $this->execute("SELECT id FROM job_queue WHERE status = 'broken' OR status = 'paused';"); + if ($rs) { + while ($row = $rs->FetchRow()) { + + // Update the priority for current job id + $query = "UPDATE job_queue SET priority = " . $currentPriority++ . + " WHERE id = '" . $row[0] . "';"; + + $rs = $this->execute($query); + if (!$rs) { + Log::error("Could not update priority for key " . $row[0]); + $result = False; + return $result; + } + + } + } + + // Then, we go through to running jobs + $rs = $this->execute("SELECT id FROM job_queue WHERE status = 'started';"); + if ($rs) { + while ($row = $rs->FetchRow()) { + + // Update the priority for current job id + $query = "UPDATE job_queue SET priority = " . $currentPriority++ . + " WHERE id = '" . $row[0] . "';"; + + $rs = $this->execute($query); + if (!$rs) { + Log::error("Could not update priority for key " . $row[0]); + $result = False; + return $result; + } + } + } + + // Then we organize the queued jobs in a way that lets us then assign + // priorities easily in a second pass + $numJobsPerUser = array(); + $userJobs = array(); + for ($i = 0; $i < $numUsers; $i++) { + // Get current username + $row = $users->FetchRow(); + $username = $row[0]; + $query = "SELECT id + FROM job_queue, job_files + WHERE job_queue.id = job_files.job AND + job_queue.username = job_files.owner AND + job_queue.username = '$username' AND + status = 'queued' + ORDER BY job_queue.queued asc, job_files.file asc"; + $rs = $this->execute($query); + if ($rs) { + $userJobs[$i] = array(); + $counter = 0; + while ($row = $rs->FetchRow()) { + $userJobs[$i][$counter++] = $row[0]; + } + $numJobsPerUser[$i] = $counter; + } + } + + // Now we can assign priorities to the queued jobs -- minimum priority is 1 + // above the priorities assigned to all other types of jobs + $maxNumJobs = max($numJobsPerUser); + for ($j = 0; $j < $maxNumJobs; $j++) { + for ($i = 0; $i < $numUsers; $i++) { + if ($j < count($userJobs[$i])) { + // Update the priority for current job id + $query = "UPDATE job_queue SET priority = " . + $currentPriority . " WHERE id = '" . + $userJobs[$i][$j] . "';"; + + $rs = $this->execute($query); + if (!$rs) { + Log::error("Could not update priority for key " . $userJobs[$i][$j]); + $result = False; + return $result; + } + $currentPriority++; + } + } + } + + // We can now return true + return $result; + } + + /** + * Logs job information in the statistics table. + * @param Job $job Job object whose information is to be logged in the + * database. + * @param string $startTime Job start time. + * @return void + */ + public function updateStatistics(Job $job, $startTime) + { + /** @var JobDescription $desc */ + $desc = $job->description(); + $parameterSetting = $desc->parameterSetting(); + $taskSetting = $desc->taskSetting(); + $analysisSetting = $desc->analysisSetting(); + + $stopTime = date("Y-m-d H:i:s"); + $id = $desc->id(); + /** @var UserV2 $user */ + $user = $desc->owner(); + $owner = $user->name(); + $group = $user->group(); + + $parameter = $parameterSetting->parameter('ImageFileFormat'); + $inFormat = $parameter->value(); + $parameter = $parameterSetting->parameter('PointSpreadFunction'); + $PSF = $parameter->value(); + $parameter = $parameterSetting->parameter('MicroscopeType'); + $microscope = $parameter->value(); + $parameter = $taskSetting->parameter('OutputFileFormat'); + $outFormat = $parameter->value(); + $parameter = $analysisSetting->parameter('ColocAnalysis'); + $colocAnalysis = $parameter->value(); + + $query = "insert into statistics values ('$id', '$owner', '$group', " . + "'$startTime', '$stopTime', '$inFormat', '$outFormat', " . + "'$PSF', '$microscope', '$colocAnalysis')"; + + $this->execute($query); + + } + + /** + * Flattens a multi-dimensional array. + * @param array $anArray Multi-dimensional array. + * @return array Flattened array. + */ + public function flatten($anArray) + { + $result = array(); + foreach ($anArray as $row) { + $result[] = end($row); + } + return $result; + } + + /** + * Returns the possible values for a given parameter. + * @param Parameter $parameter Parameter object. + * @return array Flattened array of possible values. + */ + public function readPossibleValues($parameter) + { + $name = $parameter->name(); + $query = "select value from possible_values where parameter = '$name';"; + $answer = $this->query($query); + $result = $this->flatten($answer); + return $result; + } + + /** + * Returns the translated possible values for a given parameter. + * @param Parameter $parameter Parameter object. + * @return array Flattened array of translated possible values. + */ + public function readTranslatedPossibleValues($parameter) + { + $name = $parameter->name(); + $query = "select translation from possible_values where parameter = '$name';"; + $answer = $this->query($query); + $result = $this->flatten($answer); + return $result; + } + + /** + * Returns the translation of current value for a given parameter. + * @param string $parameterName Name of the Parameter object. + * @param string $value Value for which a translation should be returned. + * @return string Translated value. + */ + public function translationFor($parameterName, $value) + { + $query = "select translation from possible_values where parameter = '$parameterName' and value = '$value';"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Returns the translation of a hucore value. + * @param string $parameterName Name of the Parameter object. + * @param string $hucorevalue Value name in HuCore. + * @return string Expected value by HRM. + */ + public function hucoreTranslation($parameterName, $hucorevalue) + { + $query = "select value from possible_values where parameter = '" . $parameterName . "' and translation = '" . $hucorevalue . "'"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Returns an array of all file extensions. + * @return array Array of file extensions. + */ + public function allFileExtensions() + { + $query = "select distinct extension from file_extension"; + $answer = $this->query($query); + $result = $this->flatten($answer); + return $result; + } + + /** + * Returns an array of all extensions for multi-dataset files. + * @return array Array of file extensions for multi-dataset files. + */ + public function allMultiFileExtensions() + { + $query = "SELECT name FROM file_format, file_extension + WHERE file_format.name = file_extension.file_format + AND file_format.ismultifile LIKE 't'"; + $answer = $this->query($query); + $result = $this->flatten($answer); + return $result; + } + + /** + * Returns an array of file extensions associated to a given file format. + * @param string $imageFormat File format. + * @return array Array of file extensions. + */ + public function fileExtensions($imageFormat) + { + $query = "select distinct extension from file_extension where file_format = '$imageFormat';"; + $answer = $this->query($query); + $result = $this->flatten($answer); + return $result; + } + + /** + * Returns all restrictions for a given numerical parameter. + * @param Parameter $parameter Parameter (object). + * @return array Array of restrictions. + */ + public function readNumericalValueRestrictions(Parameter $parameter) + { + $name = $parameter->name(); + $query = "select min, max, min_included, max_included, standard from boundary_values where parameter = '$name';"; + $result = $this->queryLastRow($query); + if (!$result) { + $result = array(null, null, null, null, null); + } + return $result; + } + + /** + * Returns the file formats that fit the conditions expressed by the + * parameters. + * @param bool $isSingleChannel Set whether the file format must be single + * channel (True), multi channel (False) or if it doesn't matter (NULL). + * @param bool $isVariableChannel Set whether the number of channels must be + * variable (True), fixed (False) or if it doesn't matter (NULL). + * @param bool $isFixedGeometry Set whether the geometry (xyzt) must be fixed + * (True), variable (False) or if it doesn't matter (NULL). + * @return array Array of file formats. + * @todo Check if this method is still used. + */ + public function fileFormatsWith($isSingleChannel, $isVariableChannel, $isFixedGeometry) + { + $isSingleChannelValue = 'f'; + $isVariableChannelValue = 'f'; + $isFixedGeometryValue = 'f'; + if ($isSingleChannel) { + $isSingleChannelValue = 't'; + } + if ($isVariableChannel) { + $isVariableChannelValue = 't'; + } + if ($isFixedGeometry) { + $isFixedGeometryValue = 't'; + } + $conditions = array(); + if ($isSingleChannel != NULL) { + $conditions['isSingleChannel'] = $isSingleChannelValue; + } + if ($isVariableChannel != NULL) { + $conditions['isVariableChannel'] = $isVariableChannelValue; + } + if ($isFixedGeometry != NULL) { + $conditions['isFixedGeometry'] = $isFixedGeometryValue; + } + return $this->retrieveColumnFromTableWhere('name', 'file_format', $conditions); + } + + /** + * Returns the geometries (XY, XY-time, XYZ, XYZ-time) fit the conditions + * expressed by the parameters. + * @param bool $isThreeDimensional True if 3D. + * @param bool $isTimeSeries True if time-series. + * @return array Array of geometries. + * @todo Check if this method is still used. + */ + public function geometriesWith($isThreeDimensional, $isTimeSeries) + { + $isThreeDimensionalValue = 'f'; + $isTimeSeriesValue = 'f'; + if ($isThreeDimensional) { + $isThreeDimensionalValue = 't'; + } + if ($isTimeSeries) { + $isTimeSeriesValue = 't'; + } + $conditions = array(); + if ($isThreeDimensional != NULL) { + $conditions['isThreeDimensional'] = $isThreeDimensionalValue; + } + if ($isTimeSeries != NULL) { + $conditions['isTimeSeries'] = $isTimeSeriesValue; + } + return $this->retrieveColumnFromTableWhere("name", "geometry", $conditions); + } + + /** + * Return all values from the column from the table where the condition + * evaluates to true. + * @param string $column Name of the column from which the values are taken + * @param string $table Name of the table from which the values are taken + * @param array $conditions Array of conditions that the result values must + * fulfill. This is an array with column names as indices and boolean values + * as content. + * @return array Array of values. + */ + public function retrieveColumnFromTableWhere($column, $table, $conditions) + { + $query = "select distinct $column from $table where "; + foreach ($conditions as $eachName => $eachValue) { + $query = $query . $eachName . " = '" . $eachValue . "' and "; + } + $query = $query . "1 = 1"; + $answer = $this->query($query); + $result = array(); + + if (!empty($answer)) { + foreach ($answer as $row) { + $result[] = end($row); + } + } + + return $result; + } + + /** + * Returns the default value for a given parameter. + * @param string $parameterName Name of the parameter. + * @return string Default value. + */ + public function defaultValue($parameterName) + { + $query = "SELECT value FROM possible_values WHERE " . + "parameter='$parameterName' AND isDefault='t'"; + $result = $this->queryLastValue($query); + if ($result === False) { + return NULL; + } + + return $result; + } + + /** + * Returns the id for next job from the queue, sorted by priority. + * @return string Job id. + */ + public function getNextIdFromQueue() + { + // For the query we join job_queue and job_files, since we want to sort also by file name + $query = "SELECT id + FROM job_queue, job_files + WHERE job_queue.id = job_files.job AND job_queue.username = job_files.owner + AND job_queue.status = 'queued' + ORDER BY job_queue.priority desc, job_queue.status desc, job_files.file desc;"; + $result = $this->queryLastValue($query); + if (!$result) { + return NULL; + } + return $result; + } + + /** + * Returns all jobs from the queue, both compound and simple, ordered by + * priority. + * @return array All jobs. + */ + public function getQueueJobs() + { + // Get jobs as they are in the queue, compound or not, without splitting + // them. + $query = "SELECT id, username, queued, start, server, process_info, status + FROM job_queue + ORDER BY job_queue.priority asc, job_queue.queued asc, job_queue.status asc;"; + $result = $this->query($query); + return $result; + } + + /** + * Returns all jobs from the queue, both compound and simple, and the + * associated file names, ordered by priority. + * @return array All jobs. + */ + public function getQueueContents() + { + // For the query we join job_queue and job_files, since we want to sort also by file name + $query = "SELECT id, username, queued, start, stop, server, process_info, status, file + FROM job_queue, job_files + WHERE job_queue.id = job_files.job AND job_queue.username = job_files.owner + ORDER BY job_queue.priority asc, job_queue.queued asc, job_queue.status asc, job_files.file asc + LIMIT 100"; + $result = $this->query($query); + return $result; + } + + /** + * Returns all jobs from the queue for a given id (that must be unique!) + * @param string $id Id of the job. + * @return array All jobs for the id + */ + public function getQueueContentsForId($id) + { + $query = "select id, username, queued, start, server, process_info, status from job_queue where id='$id';"; + $result = $this->queryLastRow($query); // it is supposed that just one job exists with a given id + return $result; + } + + /** + * Returns all file names associated to a job with given id. + * @param string $id Job id. + * @return array Array of file names. + */ + public function getJobFilesFor($id) + { + $query = "select file from job_files where job = '" . $id . "'"; + $result = $this->query($query); + $result = $this->flatten($result); + return $result; + } + + /** + * Returns the file series mode of a job with given id. + * @param string $id Job id + * @return bool True if file series, false otherwise. + */ + public function getSeriesModeForId($id) + { + $query = "select autoseries from job_files where job = '$id';"; + $result = $this->queryLastValue($query); + + return $result; + } + + /** + * Returns the name of the user who created the job with given id. + * @param string $id SId of the job. + * @return string Name of the user. + */ + public function userWhoCreatedJob($id) + { + $query = "select username from job_queue where id = '$id';"; + $result = $this->queryLastValue($query); + if (!$result) { + return NULL; + } + return $result; + } + + /** + * Deletes job with specified ID from all job tables. + * @param string $id Id of the job. + * @return bool True if success, false otherwise. + */ + public function deleteJobFromTables($id) + { + // TODO: Use foreign keys in the database! + $result = True; + $result = $result && $this->execute( + "delete from job_analysis_parameter where setting='$id';"); + $result = $result && $this->execute( + "delete from job_analysis_setting where name='$id';"); + $result = $result && $this->execute( + "delete from job_files where job='$id';"); + $result = $result && $this->execute( + "delete from job_parameter where setting='$id';"); + $result = $result && $this->execute( + "delete from job_parameter_setting where name='$id';"); + $result = $result && $this->execute( + "delete from job_queue where id='$id';"); + $result = $result && $this->execute( + "delete from job_task_parameter where setting='$id';"); + $result = $result && $this->execute( + "delete from job_task_setting where name='$id';"); + return $result; + } + + /** + * Returns the path to hucore on given host. + * @param string $host Host name. + * @return string Full path to hucore. + * @todo Better management of multiple hosts. + */ + function huscriptPathOn($host) + { + $query = "SELECT huscript_path FROM server where name = '$host'"; + $result = $this->queryLastValue($query); + if (!$result) { + return NULL; + } + return $result; + } + + /** + * Get the name of a free server. + * @return string Name of the free server. + */ + public function freeServer() + { + $query = "select name from server where status='free'"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Get the status (i.e. free, busy, paused) of server with given name. + * @param string $name Name of the server. + * @return string Status (one of 'free', 'busy', or 'paused'). + */ + public function statusOfServer($name) + { + $query = "select status from server where name='$name'"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Checks whether server is busy. + * @param string $name Name of the server. + * @return bool True if the server is busy, false otherwise + */ + public function isServerBusy($name) + { + $status = $this->statusOfServer($name); + $result = ($status == 'busy'); + return $result; + } + + /** + * Checks whether the switch in the queue manager is 'on'. + * @return bool True if switch is on, false otherwise. + */ + public function isSwitchOn() + { + // Handle some back-compatibility issue + if ($this->doGlobalVariablesExist()) { + $query = "SELECT value FROM queuemanager WHERE field = 'switch'"; + $answer = $this->queryLastValue($query); + $result = True; + if ($answer == 'off') { + $result = False; + Log::warning("$query; returned '$answer'"); + Util::notifyRuntimeError("hrmd stopped", + "$query; returned '$answer'\n\nThe HRM queue manage will stop."); + } + } else { + $query = "select switch from queuemanager"; + $answer = $this->queryLastValue($query); + $result = True; + if ($answer == 'off') { + $result = False; + Log::warning("$query; returned '$answer'"); + Util::notifyRuntimeError("hrmd stopped", + "$query; returned '$answer'\n\nThe HRM queue manage will stop."); + } + } + + return $result; + } + + /** + * Gets the status of the queue manager's switch. + * @return string 'on' or 'off' + */ + public function getSwitchStatus() + { + if ($this->doGlobalVariablesExist()) { + $query = "SELECT value FROM queuemanager WHERE field = 'switch'"; + $answer = $this->queryLastValue($query); + } else { + $query = "select switch from queuemanager"; + $answer = $this->queryLastValue($query); + } + return $answer; + } + + /** + * Sets the status of the queue manager's switch. + * @param string $status Either 'on' or 'off' + * @return array Query result. + */ + public function setSwitchStatus($status) + { + $result = $this->execute("UPDATE queuemanager SET value = '$status' WHERE field = 'switch'"); + return $result; + } + + /** + * Sets the state of the server to 'busy' and the pid for a running job. + * @param string $name Server name. + * @param string $pid Process identifier associated with a running job. + * @return array Query result. + */ + public function reserveServer($name, $pid) + { + $query = "update server set status='busy', job='$pid' where name='$name'"; + $result = $this->execute($query); + return $result; + } + + /** + * Sets the state of the server to 'free' and deletes the the pid. + * @param string $name Server name. + * @param string $pid Process identifier associated with a running job (UNUSED!). + * @return array Query result. + */ + public function resetServer($name, $pid) + { + $query = "update server set status='free', job=NULL where name='$name'"; + $result = $this->execute($query); + return $result; + } + + /** + * Starts a job. + * @param Job $job Job object. + * @return array Query result. + */ + public function startJob(Job $job) + { + $desc = $job->description(); + $id = $desc->id(); + $server = $job->server(); + $process_info = $job->pid(); + $query = "update job_queue set start=NOW(), server='$server', process_info='$process_info', status='started' where id='$id'"; + $result = $this->execute($query); + return $result; + } + + /** + * Get all running jobs. + * @return array Array of Job objects. + */ + public function getRunningJobs() + { + $result = array(); + $query = "select id, process_info, server from job_queue where status = 'started'"; + $rows = $this->query($query); + if (!$rows) return $result; + + foreach ($rows as $row) { + $desc = new JobDescription(); + $desc->setId($row['id']); + $desc->load(); + $job = new Job($desc); + $job->setServer($row['server']); + $job->setPid($row['process_info']); + $job->setStatus('started'); + $result[] = $job; + } + return $result; + } + + /** + * Get names of all processing servers (independent of their status). + * @return array Array of server names. + */ + public function availableServer() + { + $query = "select name from server"; + $result = $this->query($query); + $result = $this->flatten($result); + return $result; + } + + /** + * Get the starting time of given job object. + * @param Job $job Job object. + * @return string Start time. + */ + public function startTimeOf(Job $job) + { + $desc = $job->description(); + $id = $desc->id(); + $query = "select start from job_queue where id = '$id';"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Returns a formatted time from a unix timestamp. + * @param string $timestamp Unix timestamp. + * @return string Formatted time string: YYYY-MM-DD hh:mm:ss. + */ + public function fromUnixTime($timestamp) + { + $query = "select FROM_UNIXTIME($timestamp)"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Pauses a job of given id. + * @param string $id Job id + * @return array query result. + */ + public function pauseJob($id) + { + $query = "update job_queue set status='paused' where id='$id';"; + $result = $this->execute($query); + return $result; + } + + /** + * Sets the end time of a job. + * @param string $id Job id. + * @param string $date Formatted date: YYYY-MM-DD hh:mm:ss. + * @return array Query result. + */ + public function setJobEndTime($id, $date) + { + $query = "update job_queue set stop='$date' where id='$id'"; + $result = $this->execute($query); + return $result; + } + + /** + * Changes status of 'paused' jobs to 'queued'. + * @return array Query result + */ + public function restartPausedJobs() + { + $query = "update job_queue set status='queued' where status='paused'"; + $result = $this->execute($query); + return $result; + } + + /** + * Marks a job with given id as 'broken' (i.e. to be removed). + * @param string $id Job id. + * @return array Query result. + */ + public function markJobAsRemoved($id) + { + $query = "update job_queue set status='broken' where (status='queued' or status='paused') and id='$id';"; + // $query = "update job_queue set status='broken' where id='" . $id . "'"; + $result = $this->execute($query); + $query = "update job_queue set status='kill' where status='started' and id='$id';"; + $result = $this->execute($query); + return $result; + } + + /** + * Set the server status to free. + * @param string $server Server name. + * @return array Query result. + */ + public function markServerAsFree($server) + { + $query = "update server set status='free', job=NULL where name='$server'"; + $result = $this->execute($query); + return $result; + } + + /** + * Get all jobs with status 'broken'. + * @return array Array of ids for broken jobs. + */ + public function getMarkedJobIds() + { + $conditions['status'] = 'broken'; + $ids = $this->retrieveColumnFromTableWhere('id', 'job_queue', $conditions); + return $ids; + } + + /** + * Get all jobs with status 'kill' to be killed by the Queue Manager. + * @return array Array of ids for jobs to be killed. + */ + public function getJobIdsToKill() + { + $conditions['status'] = 'kill'; + $ids = $this->retrieveColumnFromTableWhere('id', 'job_queue', $conditions); + return $ids; + } + + /** + * Return the list of known users (without the administrator). + * @param string String User name to filter out from the list (optional). + * @return array Filtered array of users. + */ + public function getUserList($name) + { + $query = "select name from username where name != '$name' " . + " and name != 'admin';"; + $result = $this->query($query); + return $result; + } + + /** + * Get the name of the user who owns a job with given id. + * @param string $id Job id. + * @return string Name of the user who owns the job. + */ + public function getJobOwner($id) + { + $query = "select username from job_queue where id = '$id'"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Returns current database (!) date and time. + * @return string formatted date (YYYY-MM-DD hh:mm:ss). + */ + public function now() + { + $query = "select now()"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Returns the group to which the user belongs. + * @param string $userName Name of the user + * @return string Group name. + */ + public function getGroup($userName) + { + $query = "SELECT research_group FROM username WHERE name= '$userName'"; + $result = $this->queryLastValue($query); + return $result; + } + + /** + * Updates the e-mail address of a user. + * @param string $userName Name of the user. + * @param string $email E-mail address. + * @return array Query result. + */ + public function updateMail($userName, $email) + { + $cmd = "UPDATE username SET email = '$email' WHERE name = '$userName'"; + $result = $this->execute($cmd); + return $result; + } + + /** + * Gets the maximum number of channels from the database. + * @return int The number of channels. + */ + public function getMaxChanCnt() + { + $query = "SELECT MAX(CAST(value AS unsigned)) as \"\""; + $query .= "FROM possible_values WHERE parameter='NumberOfChannels'"; + $result = trim($this->execute($query)); + + if (!is_numeric($result)) { + $result = 5; + } + + return $result; + } + + + /** + * Get the list of settings for the user with given name from the given + * settings table. + * + * The Parameter values are not loaded. + * @param string $username Name of the user. + * @param string $table Name of the settings table. + * @return array Array of settings + */ + public function getSettingList($username, $table) + { + $query = "select name, standard from $table where owner ='$username' order by name"; + return ($this->query($query)); + } + + /** + * Get the parameter confidence level for given file format. + * @param string $fileFormat File format for which the Parameter confidence + * level is queried (not strictly necessary for the Parameters with + * confidence level 'Provide', could be set to '' for those). + * @param string $parameterName Name of the Parameter the confidence level + * should be returned. + * @return string Parameter confidence level. + */ + public function getParameterConfidenceLevel($fileFormat, $parameterName) + { + // Some Parameters MUST be provided by the user and cannot be overridden + // by the file metadata + switch ($parameterName) { + case 'ImageFileFormat' : + case 'NumberOfChannels' : + case 'PointSpreadFunction': + case 'MicroscopeType' : + case 'CoverslipRelativePosition': + case 'PerformAberrationCorrection': + case 'AberrationCorrectionMode': + case 'AdvancedCorrectionOptions': + case 'PSF' : + return "provided"; + case 'Binning': + case 'IsMultiChannel': + case 'ObjectiveMagnification': + case 'CMount': + case 'TubeFactor': + case 'AberrationCorrectionNecessary': + case 'CCDCaptorSize': + case 'PSFGenerationDepth': + return "default"; + default: + + // For the other Parameters, the $fileFormat must be specified + if (($fileFormat == '') && ($fileFormat == null)) { + exit("Error: please specify a file format!" . "\n"); + } + + // The wavelength and voxel size parameters have a common + // confidence in the HRM but two independent confidences + // in hucore + if (($parameterName == "ExcitationWavelength") || + ($parameterName == "EmissionWavelength") + ) { + + $confidenceLevelEx = $this->huCoreConfidenceLevel( + $fileFormat, "ExcitationWavelength"); + $confidenceLevelEm = $this->huCoreConfidenceLevel( + $fileFormat, "EmissionWavelength"); + $confidenceLevel = $this->minConfidenceLevel( + $confidenceLevelEx, $confidenceLevelEm); + + } elseif (($parameterName == "CCDCaptorSizeX") || + ($parameterName == "ZStepSize") + ) { + + $confidenceLevelX = $this->huCoreConfidenceLevel( + $fileFormat, "CCDCaptorSizeX"); + $confidenceLevelZ = $this->huCoreConfidenceLevel( + $fileFormat, "ZStepSize"); + $confidenceLevel = $this->minConfidenceLevel( + $confidenceLevelX, $confidenceLevelZ); + + } else { + + $confidenceLevel = $this->huCoreConfidenceLevel( + $fileFormat, $parameterName); + + } + + // Return the confidence level + return $confidenceLevel; + + } + + } + + /** + * Finds out whether a Huygens module is supported by the license. + * @param string $feature The module to find out about. It can use (SQL) + * wildcards. + * @return bool True if the module is supported by the license, false + * otherwise. + */ + public function hasLicense($feature) + { + + $query = "SELECT feature FROM hucore_license WHERE " . + "feature LIKE '$feature' LIMIT 1;"; + + if ($this->queryLastValue($query) === FALSE) { + return false; + } else { + return true; + } + } + + /** + * Checks whether Huygens Core has a valid license. + * @return bool True if the license is valid, false otherwise. + */ + public function hucoreHasValidLicense() + { + + // We (ab)use the hasLicense() method + return ($this->hasLicense("freeware") == false); + } + + /** + * Gets the licensed server type for Huygens Core. + * @return string One of desktop, small, medium, large, extreme. + */ + public function hucoreServerType() + { + + $query = "SELECT feature FROM hucore_license WHERE feature LIKE 'server=%';"; + $server = $this->queryLastValue($query); + if ($server == false) { + return "no server information"; + } + return substr($server, 7); + } + + /** + * Updates the database with the current HuCore license details. + * @param string $licDetails A string with the supported license features. + * @return bool True if the license details were successfully saved, false + * otherwise. + */ + public function storeLicenseDetails($licDetails) + { + + $licStored = true; + + // Make sure that the hucore_license table exists. + $tables = $this->connection->MetaTables("TABLES"); + if (!in_array("hucore_license", $tables)) { + $msg = "Table hucore_license does not exist! " . + "Please update the database!"; + Log::error($msg); + exit($msg); + } + + // Empty table: remove existing values from older licenses. + $query = "DELETE FROM hucore_license"; + $result = $this->execute($query); + + if (!$result) { + Log::error("Could not store license details in the database!\n"); + $licStored = false; + return $licStored; + } + + // Populate the table with the new license. + $features = explode(" ", $licDetails); + foreach ($features as $feature) { + + Log::info("Storing license feature: " . $feature . PHP_EOL); + + switch ($feature) { + case 'desktop': + case 'small': + case 'medium': + case 'large': + case 'extreme': + $feature = "server=" . $feature; + Log::info("Licensed server: $feature"); + break; + default: + Log::info("Licensed feature: $feature"); + } + + $query = "INSERT INTO hucore_license (feature) VALUES ('$feature')"; + $result = $this->execute($query); + + if (!$result) { + Log::error("Could not store license feature + '$feature' in the database!\n"); + $licStored = false; + break; + } + } + + return $licStored; + } + + /** + * Store the confidence levels returned by huCore into the database for + * faster retrieval. + * + * This is a rather low-level function that creates the table if needed. + * + * @param array $confidenceLevels Array of confidence levels with file + * formats as keys. + * @return bool True if storing (or updating) the database was successful, + * false otherwise. + */ + public function storeConfidenceLevels($confidenceLevels) + { + + // Make sure that the confidence_levels table exists + $tables = $this->connection->MetaTables("TABLES"); + if (!in_array("confidence_levels", $tables)) { + $msg = "Table confidence_levels does not exist! " . + "Please update the database!"; + Log::error($msg); + exit($msg); + } + + // Get the file formats + $fileFormats = array_keys($confidenceLevels); + + // Go over all $confidenceLevels and set the values + foreach ($fileFormats as $format) { + + // If the row for current $fileFormat does not exist, INSERT a new + // row with all parameters, otherwise UPDATE the existing one. + $query = "SELECT fileFormat FROM confidence_levels WHERE " . + "fileFormat = '" . $format . "' LIMIT 1;"; + + if ($this->queryLastValue($query) === FALSE) { + + // INSERT + if (!$this->connection->AutoExecute("confidence_levels", + $confidenceLevels[$format], "INSERT") + ) { + $msg = "Could not insert confidence levels for file format $format!"; + Log::error($msg); + exit($msg); + } + + } else { + + // UPDATE + if (!$this->connection->AutoExecute("confidence_levels", + $confidenceLevels[$format], 'UPDATE', + "fileFormat = '$format'") + ) { + $msg = "Could not update confidence levels for file format $format!"; + Log::error($msg); + exit($msg); + } + + } + + } + + return true; + + } + + + /** + * Get the state of GPU acceleration (as string). + * @return string One "true" or "false". + */ + public function getGPUID($server) + { + $query = "SELECT gpuId FROM server WHERE name = '$server';"; + + $result = $this->queryLastValue($query); + + return intval($result); + } + + + /** + * Add a server (including GPU info) to the list of processing machines + for the queue manager. + * @return integer > 0 on failure; 0 on success. + */ + public function addServer($serverName, $huPath, $gpuId) + { + if (!is_numeric($gpuId)) { + return "error: invalid GPU ID"; + } + + /* This allows for multiple entries for the same machine. */ + /* The queue manager only looks at the machine name and rejecting + anything after the blank. */ + $server = "$serverName $gpuId"; + + $query = "INSERT INTO server VALUES " . + "('$server','$huPath','free','NULL','$gpuId')"; + $result = $this->queryLastValue($query); + + return intval($result); + } + + + /** + * Remove a server from the list of processing machines for the queue + manager. + * @return integer > 0 on failure; 0 on success. + */ + public function removeServer($serverName) + { + $query = "DELETE FROM server WHERE name='$serverName';"; + $result = $this->queryLastValue($query); + + return intval($result); + } + + + public function getAllServers($server) + { + $query = "SELECT * FROM server;"; + + $result = $this->query($query); + + return $result; + } + + + /* ------------------------ PRIVATE FUNCTIONS --------------------------- */ + + /** + * Return the minimum of two confidence levels. + * + * The order of the levels is as follows: + * + * 'default' < 'estimated' < 'reported' < 'verified' < 'asIs'. + * + * @param string $level1 One of 'default', 'estimated', 'reported', + * 'verified', 'asIs'. + * @param string $level2 One of 'default', 'estimated', 'reported', + * 'verified', 'asIs'. + * @return string The minimum of the two confidence levels. + */ + private function minConfidenceLevel($level1, $level2) + { + $levels = array(); + $levels['default'] = 0; + $levels['estimated'] = 1; + $levels['reported'] = 2; + $levels['verified'] = 3; + $levels['asIs'] = 3; + + if ($levels[$level1] <= $levels[$level2]) { + return $level1; + } else { + return $level2; + } + + } + + /** + * Returns the raw HuCore confidence level. + * @param string $fileFormat HRM's file format. + * @param string $parameterName Name of the HRM Parameter. + * @return string HuCore's raw confidence level. + */ + private function huCoreConfidenceLevel($fileFormat, $parameterName) + { + + // Get the mapped file format + $query = "SELECT hucoreName FROM file_format WHERE name = '" . + $fileFormat . "' LIMIT 1"; + $hucoreFileFormat = $this->queryLastValue($query); + if (!$hucoreFileFormat) { + Log::warning("Could not get the mapped file name for " . $fileFormat . "!"); + return "default"; + } + + // Use the mapped file format to retrieve the + if (!array_key_exists($parameterName, $this->parameterNameDictionary)) { + return "default"; + } + $query = "SELECT " . $this->parameterNameDictionary[$parameterName] . + " FROM confidence_levels WHERE fileFormat = '" . $hucoreFileFormat . + "' LIMIT 1;"; + $confidenceLevel = $this->queryLastValue($query); + if (!$confidenceLevel) { + Log::warning("Could not get the confidence level for " . $fileFormat . "!"); + return "default"; + } + + // return the confidence level + return $confidenceLevel; + } + + /** + * Ugly hack to check for old table structure. + * @return bool True if global variables table exists, false otherwise. + */ + private function doGlobalVariablesExist() + { + global $db_type; + global $db_host; + global $db_name; + global $db_user; + global $db_password; + + $test = False; + + $dsn = $db_type . "://" . $db_user . ":" . $db_password . "@" . $db_host . "/" . $db_name; + /** @var ADODB_mysql|\ADODB_postgres8|\ADODB_postgres9 $db */ + $db = ADONewConnection($dsn); + if (!$db) + return False; + $tables = $db->MetaTables("TABLES"); + if (in_array("global_variables", $tables)) + $test = True; + + return $test; + } + +} diff --git a/inc/FileBrowser.inc.php b/inc/FileBrowser.php similarity index 64% rename from inc/FileBrowser.inc.php rename to inc/FileBrowser.php index 04ca7678d..135339028 100644 --- a/inc/FileBrowser.inc.php +++ b/inc/FileBrowser.php @@ -1,9 +1,19 @@ getValidArchiveTypesAsString(); - $onClick = "uploadImages('$maxFile', '$maxPost', " . - "'$validExtensions')"; - $tip = 'Upload a file (or a compressed archive of files) to the ' . + $onClick = "uploadImagesAlt()"; + $tip = 'Upload one or more files (or compressed archives of files) to the ' . 'server'; $name = "upload"; break; @@ -174,6 +180,7 @@ function fileButton($type) { "jqTree/tree.jquery.js", "jquery-ui/jquery-ui-1.9.1.custom.js", "jquery-ui/jquery.bgiframe-2.1.2.js", + "fineuploader/jquery.fine-uploader.js", "omero.js"); if (!isset($operationResult)) { @@ -335,6 +342,10 @@ function imageAction (list) { // The destination folder needs parsing to locate the previews. $pathAndFile = $pdir . "/" . $file; preg_match($pattern, $pathAndFile, $matches); + if (count($matches) == 0 ) { + unset($files[$key]); + continue; + } $filePreview = $matches[1] . "/hrm_previews/"; $filePreview .= basename($file) . ".preview_xy.jpg"; @@ -375,7 +386,7 @@ function imageAction (list) { $flag = " disabled=\"disabled\""; } -include("header.inc.php"); +include("header_fb.inc.php"); @@ -389,7 +400,7 @@ function imageAction (list) { if (isset($top_nav_left)) { echo $top_nav_left; } else { - wiki_link('HuygensRemoteManagerHelpFileManagement'); + echo(Nav::linkWikiPage('HuygensRemoteManagerHelpFileManagement')); } ?> @@ -403,16 +414,16 @@ function imageAction (list) { // was not the dashboard (home.php) but e.g. the "Select // images" when creating a new job. if ( strpos( $referer, 'home.php' ) === False ) { - include("./inc/nav/back.inc.php"); + echo(Nav::linkBack($referer)); } } if ( $browse_folder == "dest" ) { - include("./inc/nav/files_raw.inc.html"); + echo(Nav::linkRawImages()); } else { - include("./inc/nav/files_results.inc.html"); + echo(Nav::linkResults()); } - include("./inc/nav/user.inc.php"); - include("./inc/nav/home.inc.php"); + echo(Nav::textUser($_SESSION['user']->name())); + echo(Nav::linkHome(Util::getThisPageName())); ?> @@ -449,7 +460,7 @@ function imageAction (list) {
- - - - - - - - - -

Disclaimer: - HRM cannot guarantee that OME-TIFFs - provided by OMERO contain the original metadata.

- - - - - -
- - - -
- Your OMERO data - -
-

- - - - loggedIn) { - ?> +
+ + + + + + + + + + + + +

Disclaimer: + HRM cannot guarantee that OME-TIFFs + provided by OMERO contain the original metadata.

+ + + + - +
+

-  \n"; - ?> + -
+ loggedIn) { + ?> -
+ + +  \n"; + ?> + + + + + + + + + diff --git a/omero_actions.php b/omero_actions.php index 25fa78022..6bcdb5da3 100644 --- a/omero_actions.php +++ b/omero_actions.php @@ -4,6 +4,8 @@ // This is for the 'OMERO Data' button. +use hrm\OmeroConnection; + if ($omero_transfers && !$_SESSION['user']->isAdmin()) { if ( $browse_folder == "src" ) { @@ -63,5 +65,3 @@ $_SESSION['fileserver']->resetDestFiles(); } } - -?> diff --git a/omero_treeloader.php b/omero_treeloader.php index 9dba39e0b..1b73ebf9b 100644 --- a/omero_treeloader.php +++ b/omero_treeloader.php @@ -7,14 +7,15 @@ // gets expanded by the user. It ensures the user is logged in, has a valid // OMERO connection and finally asks the connector for the JSON data. -require_once("inc/OmeroConnection.inc.php"); +require_once("inc/OmeroConnection.php"); session_start(); if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn()) { - $req = $_SERVER['REQUEST_URI']; - $_SESSION['request'] = $req; - header("Location: " . "login.php"); exit(); + $req = $_SERVER['REQUEST_URI']; + $_SESSION['request'] = $req; + header("Location: " . "login.php"); + exit(); } $omeroConnection = $_SESSION['omeroConnection']; @@ -28,5 +29,3 @@ // fetch the child nodes and return the JSON: print($omeroConnection->getChildren($node_id)); - -?> diff --git a/phpinfo.php b/phpinfo.php index 8efef13c8..77a5719fc 100644 --- a/phpinfo.php +++ b/phpinfo.php @@ -2,22 +2,27 @@ // This file is part of the Huygens Remote Manager // Copyright and license notice: see license.txt -require_once("./inc/User.inc.php"); -require_once("./inc/wiki_help.inc.php"); +use hrm\Nav; +use hrm\Util; + +require_once dirname(__FILE__) . '/inc/bootstrap.php'; session_start(); if (isset($_GET['home'])) { - header("Location: " . "home.php"); exit(); + header("Location: " . "home.php"); + exit(); } if (!isset($_SESSION['user']) || !$_SESSION['user']->isLoggedIn() || - !$_SESSION['user']->isAdmin()) { - header("Location: " . "login.php"); exit(); + !$_SESSION['user']->isAdmin() +) { + header("Location: " . "login.php"); + exit(); } if (isset($_SERVER['HTTP_REFERER'])) { - $_SESSION['referer'] = $_SERVER['HTTP_REFERER']; + $_SESSION['referer'] = $_SERVER['HTTP_REFERER']; } $message = ""; @@ -31,16 +36,16 @@ ob_end_clean(); $matches = array(); // Get the body content -preg_match ('%(.*?)%s', $phpinfo, $matches); -$info = $matches[ 1 ]; +preg_match('%(.*?)%s', $phpinfo, $matches); +$info = $matches[1]; // "Resize" tables -$info = str_replace( 'width="600"', 'width="100%"', $info ); +$info = str_replace('width="600"', 'width="100%"', $info); // Correct the HTML -$info = preg_replace('%%s', "", $info ); -$info = preg_replace('%%s', "", $info ); -$info = preg_replace('%%s', "", $info); +$info = preg_replace('%
-
+
-

Summary -  Extended system summary

+

Summary +  Extended system summary

-
+
-
+
-
+
-
+
-
+
-

Quick help

+

Quick help

-

This page displays extended information about your PHP - installation.

+

This page displays extended information about your PHP + installation.

-
+
-
- + $message

"; + echo "

$message

"; -?> -
+ ?> +
-
+ isReachable()) { - if ($db->emailAddress($clean["username"]) == "") { - $id = get_rand_id(10); - $result = $db->addNewUser($clean["username"], - $clean["pass1"], $clean["email"], $clean["group"], $id); - - // TODO refactor - if ($result) { - $text = "New user registration:\n\n"; - $text .= "\t Username: " . $clean["username"] . "\n"; - $text .= "\t E-mail address: " . $clean["email"] . "\n"; - $text .= "\t Group: " . $clean["group"] . "\n"; - $text .= "\tRequest message: " . $clean["note"] . "\n\n"; - $text .= "Accept or reject this user here (login required)\n"; - $text .= $hrm_url . "/user_management.php?seed=" . $id; - $mail = new Mail($email_sender); - $mail->setReceiver($email_admin); - $mail->setSubject("New HRM user registration"); - $mail->setMessage($text); - if ($mail->send()) { - $notice = "Application successfully sent!\n" . - "Your application will be processed by the " . - "administrator and you will receive a confirmation " . - "by e-mail."; - } else { - $notice = "Your application was successfully stored, " . - "but there was an error e-mailing the administrator! " . - "Please " . - "inform the person in charge"; - } - $processed = True; - } else { - $message = "Could not add user to database.
" . - "Please " . - "inform the person in charge"; - } - } else $message = "This user name is already in use. Please " . - "enter another one"; - } else $message = "Database error.
" . - "Please " . - "inform the person in charge"; - } else $message = "Passwords do not match"; - } else $message = "Please fill in both password fields"; - } else $message = "Research group empty"; - } else $message = "Error in Email field.
Please fill in the email field with a valid address"; - } else $message = "Error in Name field.
Names should be < 30 characters
and contain no spaces"; + if ($clean["username"] == "") { + $message = "Please provide a valid user name."; + } else if ($clean["email"] == "") { + $message = "Please provide a valid e-mail address."; + } else if ($clean["group"] == "") { + $message = "Please provide a group."; + } else if ($clean["pass1"] == "" || $clean["pass2"] == "") { + $message = "Please provide your password (twice)."; + } else if ($clean["pass1"] == $clean["pass2"]) { + + // Make sure that there is no user with same name + if (UserManager::existsUserWithName($clean['username'])) { + + $name = $clean['username']; + $message = "Sorry, a user with name $name exists already!"; + + } else { + + // Create the new user + $institution_id = 1; + $uniqueId = UserManager::generateUniqueId(); + $result = UserManager::createUser($clean["username"], + $clean["pass1"], $clean["email"], $clean["group"], + $institution_id, "integrated", + UserConstants::ROLE_USER, + UserConstants::STATUS_ACTIVE, + $uniqueId); + + // TODO refactor + if ($result) { + $text = "New user registration:\n\n"; + $text .= "\t Username: " . $clean["username"] . "\n"; + $text .= "\t E-mail address: " . $clean["email"] . "\n"; + $text .= "\t Group: " . $clean["group"] . "\n"; + $text .= "\tRequest message: " . $clean["note"] . "\n\n"; + $text .= "Accept or reject this user here (login required)\n"; + $text .= $hrm_url . "/user_management.php?seed=" . $uniqueId; + $mail = new Mail($email_sender); + $mail->setReceiver($email_admin); + $mail->setSubject("New HRM user registration"); + $mail->setMessage($text); + if ($mail->send()) { + $notice = "Application successfully sent!\n" . + "Your application will be processed by the " . + "administrator and you will receive a confirmation " . + "by e-mail."; + } else { + $notice = "Your application was successfully stored, " . + "but there was an error e-mailing the administrator! " . + "Please contact him directly."; + } + $processed = True; + } else { + $message = "Could not create user! Please contact your administrator."; + } + } + + } else { + + $message = "Passwords do not match!"; + + } + } include("header.inc.php"); @@ -152,14 +164,14 @@ @@ -243,13 +255,13 @@ -
$notice

"; ?>
- * Required fields.

- canModifyPassword()) { + $message = "The " . $authProxy->friendlyName() . " authentication does not " . + "allow changing the password for user " . $clean['username'] . "!"; + } else { + + // Retrieve the user e-mail + $email = $authProxy->getEmailAddress($clean['username']); + + if ($email == "") { + $message = "Cannot retrieve e-mail address for user " . $clean['username'] . "!"; + } else { + + # Mark the + $seed = UserManager::generateAndSetSeed($clean['username']); + if ($seed == "") { + $message = "Could not mark the user to password reset!"; + } else { + + # Email the user with instructions + $mail = new Mail($email_sender); + $tmp_username = $clean['username']; + $text = "Please reset your password here: $hrm_url/reset_password.php?user=$tmp_username&seed=$seed"; + $mail->setReceiver($email); + $mail->setSubject("HRM's password reset"); + $mail->setMessage($text); + if (!$mail->send()) { + $notice = "Could not send an email to user $tmp_username! " . + "Please contact your administrator."; + $actionMode = "genericErrorWithMessage"; + } else { + $actionMode = "emailSentSuccessfully"; + } + } + } + } + } else { + + $message = "Sorry, could not find user " . $clean['username'] . "."; + } +} + +// If the new password has been submitted already, check it and set it +if (isset($_POST['modifypassword']) && isset($_POST['pass1']) && + isset($_POST['pass2']) && isset($_SESSION['sanizited_input'])) { + + // Retrieve the sanitized input + $clean = $_SESSION['sanizited_input']; + + // Passwords + if (Validator::isPasswordValid($_POST["pass1"])) { + $clean["pass1"] = $_POST["pass1"]; + } + + if (Validator::isPasswordValid($_POST["pass2"])) { + $clean["pass2"] = $_POST["pass2"]; + } + + if ($clean["pass1"] != $clean["pass2"]) { + $message = "Passwords do not match!"; + + # Ask again + $actionMode = "askForNewPassword"; + + } else { + + // Update the user + if (UserManager::changeUserPassword($clean['username'], $clean['pass1'])) { + + // Make the user active + UserManager::setUserStatus($clean['username'], UserConstants::STATUS_ACTIVE); + + $actionMode = "passwordUpdateSuccessful"; + + } else { + + $actionMode = "passwordUpdateFailed"; + } + + // In any case, we reset the seed + UserManager::resetSeed($clean['username']); + + } +} + +/* ***************************************************************************** + * + * DISPLAY PAGE + * + **************************************************************************** */ + +include("header.inc.php"); +?> + + +Discard changes and go back to your home page. +Query the user. +Update the password. + + + +
+ +

Account Reset your password

+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + + +
+ +
+

You can change the password for user ''.

+ + +
+ + + +
+ +
+ + +
+ +
+ + +

Congratulations!

+

Instructions have been sent to the e-mail associated to the user!

+ +

Sorry!

+

There is no matching password reset request!
+ Have you already changed your password?

+ +

Sorry!

+

Could not complete the request!
+ Please contact your administrator.

+ +

Congratulations!

+

The password was updated successfully!

+ +

Sorry!

+

Could not update the password!
+ Please contact your administrator.

+ +

Oooops!

+

Something went wrong. Please submit a bug report!

+ + +
+ +
+ +
+ +

Quick help

+ +

Here you can change your password.

+ +
+ +
+ $message

"; + ?> +
+ +
+ + diff --git a/resources/checkConfig.php b/resources/checkConfig.php index 324d65ebf..24e324397 100644 --- a/resources/checkConfig.php +++ b/resources/checkConfig.php @@ -5,7 +5,7 @@ // To use: execute from bash // $ php checkConfig.php /path/to/config/file // - // Example: php checkConfig.php /var/www/hrm/config/hrm_server.config.inc + // Example: php checkConfig.php /var/www/html/hrm/config/hrm_server_config.inc switch ($argc) { case 1: @@ -34,7 +34,7 @@ function checkConfigFile($configFile) { return; } - echo "Check against HRM v3.3.x." . PHP_EOL; + echo "Check against HRM v3.4.x." . PHP_EOL; require_once($configFile); @@ -51,7 +51,7 @@ function checkConfigFile($configFile) { "log_verbosity", "logdir", "logfile", "logfile_max_size", "send_mail", "email_sender", "email_admin", "email_list_separator", "authenticateAgainst", - "useDESEncryption", "imageProcessingIsOnQueueManager", + "imageProcessingIsOnQueueManager", "copy_images_to_huygens_server", "useThumbnails", "genThumbnails", "movieMaxSize", "saveSfpPreviews", "maxComparisonSize", "ping_command", "ping_parameter", @@ -62,7 +62,7 @@ function checkConfigFile($configFile) { "adodb", "enableUserAdmin", "allow_reservation_users", "resultImagesOwnedByUser", "resultImagesRenamed", "runningLocation", "convertBin", "enable_code_for_huygens", - "change_ownership"); + "change_ownership", "useDESEncryption"); // Check for variables that must exist $numMissingVariables = 0; @@ -82,10 +82,26 @@ function checkConfigFile($configFile) { } } - if ($numMissingVariables + $numVariablesToRemove == 0) { + // Check the values of the $authenticateAgainst variable + $numVariableToFix = 0; + global $authenticateAgainst; + if (!is_array($authenticateAgainst)) { + echo "* * * Error: variable 'authenticateAgainst' must be an array!" . PHP_EOL; + if ($authenticateAgainst == "MYSQL") { + echo "* * * Moreover, please change 'MYSQL' into 'integrated'." . PHP_EOL; + } elseif ($authenticateAgainst == "ACTIVE_DIR") { + echo "* * * Moreover, please change 'ACTIVE_DIR' into 'active_dir'." . PHP_EOL; + } elseif ($authenticateAgainst == "LDAP") { + echo "* * * Moreover, please change 'LDAP' into 'ldap'." . PHP_EOL; + } else { + // + } + $numVariableToFix = 1; + } + + if ($numMissingVariables + $numVariablesToRemove + $numVariableToFix == 0) { echo "Check completed successfully! Your configuration file is valid!" . PHP_EOL; } else { echo "Check completed with errors! Please fix your configuration!" . PHP_EOL; } } -?> diff --git a/resources/debugActiveDirectory.php b/resources/debugActiveDirectory.php new file mode 100644 index 000000000..d7287150d --- /dev/null +++ b/resources/debugActiveDirectory.php @@ -0,0 +1,71 @@ + $ACCOUNT_SUFFIX, + 'ad_port' => $AD_PORT, + 'base_dn' => $BASE_DN, + 'domain_controllers' => $DOMAIN_CONTROLLERS, + 'admin_username' => $AD_USERNAME, + 'admin_password' => $AD_PASSWORD, + 'real_primarygroup' => $REAL_PRIMARY_GROUP, + 'use_ssl' => $USE_SSL, + 'use_tls' => $USE_TLS, + 'recursive_groups' => $RECURSIVE_GROUPS); + +try { + $m_AdLDAP = new adLDAP($options); + // echo "Created adLDAP object!\n"; +} catch (adLDAPException $e) { + // Make sure to clean stack traces + $pos = stripos($e, 'AD said:'); + if ($pos !== false) { + $e = substr($e, 0, $pos); + } + echo "$e\n"; + exit(); +} + +$auth_u = $AD_USERNAME; +$auth_p = $AD_PASSWORD; + +$b = $m_AdLDAP->user()->authenticate($auth_u, $auth_p); + +if ($b === false) { + print "User '$auth_u': authentication FAILED!\n"; + $m_AdLDAP->close(); +} else { + print "User '$auth_u': successfully authenticated!\n"; +} + +$info = $m_AdLDAP->user()->infoCollection($query_u, array("mail")); +$userGroups = $m_AdLDAP->user()->groups($query_u); + +print_r($info); +print_r($userGroups); + +$m_AdLDAP->close(); diff --git a/resources/hrmd.plist.sample b/resources/hrmd.plist.sample deleted file mode 100644 index 086fd8cc1..000000000 --- a/resources/hrmd.plist.sample +++ /dev/null @@ -1,39 +0,0 @@ - - - - - EnvironmentVariables - - HRM_HOME - /Library/WebServer/Documents/hrm - HRM_DATA - /work/hrm_data - HRM_SOURCE - src - HRM_DEST - dst - - KeepAlive - - Label - hrmd - OnDemand - - ProgramArguments - - /usr/bin/php - -q - /Library/WebServer/Documents/hrm/run/runHuygensRemoteManager.php - - RunAtLoad - - StandardErrorPath - /var/log/hrm/log_error_d.txt - StandardOutPath - /var/log/hrm/log_d.txt - UserName - _www - WorkingDirectory - /Library/WebServer/Documents/hrm/run - - diff --git a/resources/ome_hrm_check_imports.py b/resources/ome_hrm_check_imports.py new file mode 100644 index 000000000..c3ebaee1a --- /dev/null +++ b/resources/ome_hrm_check_imports.py @@ -0,0 +1,32 @@ +import sys + +print "sys.version:", sys.version +print "sys.executable:", sys.executable + +import argparse +print "argparse:", argparse.__file__, "version:", argparse.__version__ + +import os +print "os:", os.__file__ + +import json +print "json:", json.__file__, "version:", json.__version__ + +import re +print "re:", re.__file__, "version:", re.__version__ + +OMERO_LIB='/opt/OMERO/OMERO.server-5.1.2-ice34-b45/lib/python' +sys.path.insert(0, OMERO_LIB) +print "sys.path: [" +for path in sys.path: + print " '%s'," % path +print "]" + +try: + import omero + print "OMERO Python bindings:", omero.__file__ + from omero.gateway import BlitzGateway + print "Successfully loaded OMERO's BlitzGateway." +except ImportError as err: + print "ERROR importing the OMERO Python bindings! Message:", err + sys.exit(1) diff --git a/resources/systemd/hrmd.service b/resources/systemd/hrmd.service index 55e313ba0..4a85cdecb 100644 --- a/resources/systemd/hrmd.service +++ b/resources/systemd/hrmd.service @@ -1,10 +1,14 @@ [Unit] Description=HRM (Huygens Remote Manager) Queue Manager Service -Requires=mariadb.service -After=mariadb.service +# For both 'Requires=' and 'After=', please set one of mysql.service, +# mariadb.service (fork of mysql), postgresql.service. +Requires=mysql.service +Wants=network-online.target +After=mysql.service network.target network-online.target [Service] -User=hrmuser +# If needed, change 'User=' and 'Group=' to point to the correct values. +User=hrm Group=hrm ExecStart=/var/www/html/hrm/bin/hrm_queuemanager --detach Type=forking diff --git a/run/runHuygensRemoteManager.php b/run/runHuygensRemoteManager.php index e8aa6d079..11cb86844 100644 --- a/run/runHuygensRemoteManager.php +++ b/run/runHuygensRemoteManager.php @@ -3,9 +3,9 @@ // Copyright and license notice: see license.txt global $isServer; +use hrm\QueueManager; + $isServer = true; -require_once('../inc/hrm_config.inc.php'); -require_once('../inc/QueueManager.inc.php'); +require_once dirname(__FILE__) . '/../inc/bootstrap.php'; $manager = new QueueManager(); $manager->run(); -?> \ No newline at end of file diff --git a/scripts/common.js b/scripts/common.js index 4b797e946..c0c027625 100644 --- a/scripts/common.js +++ b/scripts/common.js @@ -4,11 +4,11 @@ // Copyright and license notice: see license.txt var popup; -var generated = new Array(); +var generated = []; var debug = ''; var control = ''; var filemenu = '
'; -; + function clean() { if (popup != null) { popup.close(); @@ -44,7 +44,7 @@ function openWindow(url) { function openPopup(target) { var url = target + "_popup.php"; var name = "popup"; - var options = "directories = no, menubar = no, status = no, width = 560, height = 280"; + var options = "directories = no, menubar = no, status = no, width = 560, height = 380"; popup = window.open(url, name, options); popup.focus(); } @@ -318,74 +318,85 @@ function setStedEntryProperties( ) { // Grey out the SPIM input fields of a specific channel if the // corresponding excitation mode is set to 'Gaussian'. function changeSpimEntryProperties(selectObj, channel) { - var gaussBanTagArray = ["SpimNA", "SpimFill"]; - - for (var i = 0; i < gaussBanTagArray.length; i++) { - var tag = gaussBanTagArray[i]; - var id = tag.concat(channel); - var inputElement = document.getElementById(id); - - if ( selectObj.value == 'gauss' || selectObj.value == 'gaussMuVi') { - inputElement.readOnly = true; - inputElement.style.color="#000"; - inputElement.style.backgroundColor="#888"; - } else { - inputElement.readOnly = false; - inputElement.style.color="#000"; - inputElement.style.backgroundColor=""; - } + if (selectObj === undefined) { + return; } - var excFillBanTagArray = ["SpimGaussWidth"]; - - for (var i = 0; i < excFillBanTagArray.length; i++) { - var tag = excFillBanTagArray[i]; - var id = tag.concat(channel); + // Declare element + var element; - var inputElement = document.getElementById(id); - - if ( selectObj.value == 'gauss' || selectObj.value == 'gaussMuVi') { - inputElement.readOnly = false; - inputElement.style.color="#000"; - inputElement.style.backgroundColor=""; - } else { - inputElement.readOnly = true; - inputElement.style.color="#000"; - inputElement.style.backgroundColor="#888"; - } + // SpimNA + element = $("#SpimNA" + channel); + if (selectObj.value == 'gauss' || selectObj.value == 'gaussMuVi') { + element.attr('readonly', true); + element.css('background-color', "#888"); + } else { + element.attr('readonly', false); + element.css('background-color', ""); } - var dirBanTagArray = ["SpimDir"]; - - for (var i = 0; i < dirBanTagArray.length; i++) { - var tag = dirBanTagArray[i]; - var id = tag.concat(channel); + // SpimFill + element = $("#SpimFill" + channel); + if (selectObj.value == 'gauss' || selectObj.value == 'gaussMuVi') { + element.attr('readonly', true); + element.css('background-color', "#888"); + } else { + element.attr('readonly', false); + element.css('background-color', ""); + } - var inputElement = document.getElementById(id); - var length = inputElement.options.length; - - if ( selectObj.value == 'gaussMuVi') { - var option = new Option("Left + right", "left+right"); - inputElement.add(option); - var option = new Option("Top + bottom", "top+bottom"); - inputElement.add(option); - } else { - var option = new Option("From left", "left"); - inputElement.add(option); - var option = new Option("From right", "right"); - inputElement.add(option); - var option = new Option("From top", "top"); - inputElement.add(option); - var option = new Option("From bottom", "bottom"); - inputElement.add(option); - } - for (var j = length - 1; j >= 0; j--) { - inputElement.remove(j); + // SpimGaussWidth + element = $("#SpimGaussWidth" + channel); + if (selectObj.value == 'gauss' || selectObj.value == 'gaussMuVi') { + element.attr('readonly', false); + element.css('background-color', ""); + } else { + element.attr('readonly', true); + element.css('background-color', "#888"); + } + + // SpimDir + var options = $("#SpimDir" + channel + " option"); + $.each(options, function() { + + var val = $(this).text().trim(); + switch (val) { + + case "From left": + case "From right": + case "From top": + case "From bottom": + if (selectObj.value == 'gaussMuVi') { + $(this).hide(); + } else { + $(this).show(); + } + break; + case "Right + left": + case "Top + bottom": + if (selectObj.value == 'gaussMuVi') { + $(this).show(); + } else { + $(this).hide(); + } } + }); + + // If the previous value is still visible, we keep it; otherwise, we reset + // the SpimDir selection to the first visible entry + if ($("#SpimDir" + channel).find(":selected").css('display') == 'none') { + $.each(options, function() { + if ($(this).css('display') != 'none') { + $(this).prop("selected", true); + return false; + } + }); } } + + function setSpimEntryProperties( ) { var tag = "SpimExcMode"; @@ -431,6 +442,7 @@ function checkAgainstFormat(file, selectedFormat) { case 'dv': case 'ims': case 'lif': + case 'lof': case 'lsm': case 'oif': case 'pic': @@ -439,6 +451,7 @@ function checkAgainstFormat(file, selectedFormat) { case 'zvi': case 'czi': case 'nd2': + case 'nd': case 'tf2': case 'tf8': case 'btf': @@ -584,7 +597,7 @@ function confirmUpload() { form = document.getElementById("uploadForm"); if ( form.elements[0].value == '' ) { - alert("Please choose a file to upload, or cancel.") + alert("Please choose a file to upload, or cancel."); return false; } @@ -592,7 +605,7 @@ function confirmUpload() { changeDiv('upMsg', 'Please wait until your browser finishes the file transfer: do not reload or go away from this page.'); spin = '

Please wait...
' + + 'alt="busy">
Please wait...'; changeDiv('info', spin); changeDiv('actions', ''); changeDiv('buttonUpload', @@ -723,6 +736,22 @@ function uploadImages(maxFile, maxPost, archiveExt) { } +/** + * Display the new uploader based on FineUploader (http://fineuploader.com/). + */ +function uploadImagesAlt() { + + // Cancel the OMERO selection + cancelOmeroSelection(); + + // Show the uploader + var upFormID = $("#up_form"); + + // Clear the div + upFormID.show(); + +} + function downloadImages() { changeDiv('omeroSelection',''); diff --git a/scripts/fineuploader/LICENSE b/scripts/fineuploader/LICENSE new file mode 100644 index 000000000..1ac039d8f --- /dev/null +++ b/scripts/fineuploader/LICENSE @@ -0,0 +1,296 @@ +Widen Commercial License Agreement + +This Fine Uploader Subscription and Support Agreement (the “ SSA ”) is entered into +between Widen Enterprises, Inc., a Wisconsin corporation having its principal United +States offices at 6911 Mangrove Lane, Monona, WI 53713 (“ Widen ”), and the individual, +corporation or other business entity (“ Customer ”) identified as the Customer by downloading +the commercial version of Fine Uploader Software. Widen and Customer may be referred to +individually as a “ Party ” and collectively as the “ Parties ”. + +Widen offers this SSA for our commercial customers who require the use of Fine Uploader +in a commercial context. + +For purposes of this SSA, the term “ Fine Uploader Software ” shall mean Fine Uploader +software delivered or made available in source or object form. + +For purposes of this SSA, the term “ Fine Uploader Support ” shall mean software maintenance +releases and software support as described below for the Fine Uploader Software. + +This SSA shall become effective upon Customer downloading a commercial copy of Fine +Uploader Software (“ Effective Date ”). By downloading a commercial version of Fine Uploader +Software, Customer agrees to this SSA. + +Terms and Conditions + +Grant of Commercial Subscription Software License. +The license described in this section applies to Fine Uploader Software identified or +described at fineuploader.com. Widen hereby grants Customer a non-exclusive, +non-transferable, non-assignable, non-sublicenseable license (“ Subscription License ”) to +use, solely for use in the Customer’s software application, the object code of Fine Uploader +Software for the Term specified herein, and subject to the limits of use authorized for +Fine Uploader Software (the “ Limits ”). During the Term of the Subscription License for +Fine Uploader Software, Customer is authorized to create as many copies of Fine Uploader +Software as are strictly necessary to support the Limits of use authorized. + +Restrictions on Distribution and Copying. +Unless expressly authorized in writing by Widen, Fine Uploader Software provided by +Widen under this SSA may not be distributed to any other person or entity, and any such +distribution shall be deemed a copyright infringement as well as a material breach of +this SSA. + +Delivery. +Customer may obtain Fine Uploader Software by electronically downloading the Fine +Uploader Software from fineuploader.com or by performing an authorized software +update. All Fine Uploader Software shall be deemed delivered upon download, copying, +or receipt from Widen. + +Term and Termination. +The term of this SSA shall commence on the Effective Date and shall continue for a +period of twelve (12) months unless terminated earlier as set forth herein (the “ Term ”). +This SSA shall terminate at the end of the Term except for such +provisions that may be indicated herein as surviving termination of this SSA. Either +Party may terminate this SSA and the License granted hereunder upon written notice for +any material breach of this SSA, including failure to pay undisputed Fees when and as +due. In the event of termination of this SSA for any cause, all rights granted hereunder +automatically revert to the granting Party. + +If a Party breaches any of the terms of this SSA and fails to cure such breach within +fifteen (15) days of written notification of such breach (the “ Cure Period ”), +the non-breaching Party giving such notice shall have the right, without prejudice to any other +rights it may have, so long as the breach remains uncured, to terminate this SSA, +effective upon giving written notice to the breaching Party. This SSA may also be +terminated immediately by a Party upon the other Party’s bankruptcy, liquidation, +judicial management, receivership, act of insolvency or change in control. + +Fees and Limits. +The fee for Fine Uploader Software and Fine Uploader Support is an annual charge that +includes all Releases and Patches within the Term (“ Fees ”). The Fees and +Limits for Fine Uploader Software and Fine Uploader Support are available at +fineuploader.com/purchase. + +Costs and Expenses. +Except as expressly provided in this SSA, each Party shall be responsible for all costs and +expenses incurred by that Party in performing its obligations or exercising its rights under +this SSA. + +Payment Terms. +The Fees must be paid in U.S. Dollars. Customer authorizes Widen to bill Customer’s +credit card for the Fees for items specified at www.fineuploader.com. Any and all +payments made by Customer pursuant to this SSA are non-refundable unless otherwise +specified. If Customer fails to fulfill its payment obligations for undisputed Fees as +specified herein, Widen shall have the right to (a) charge Customer for any reasonable +collection costs, including attorneys’ fees; and (b) suspend or cancel performance of all +or part of this SSA. + +Taxes. +“ Taxes ” means any form of taxation, levy, duty, customs fee, charge, contribution +or impost of whatever nature and by whatever authority imposed (including without +limitation any fine, penalty, surcharge or interest), excluding any taxes based solely on +the net U.S. income of Widen. Customer shall pay to Widen an amount equal to any +Taxes arising from or relating to this SSA that are paid by or are payable by Widen +including, without limitation, sales, service, use, or value added taxes. If Customer is +required under any applicable law or regulation, domestic or foreign, to withhold or +deduct any portion of the payments due to Widen, then the sum payable to Widen shall +be increased by the amount necessary so that Widen receives an amount equal to the sum +it would have received had Customer made no withholdings or deductions. Customers +with a tax-exempt status shall provide to Widen documentation of such status sufficient +for Widen and Customer to avoid liability for qualifying Taxes. + +Limited Warranty and WARRANTY DISCLAIMER for Fine Uploader Software. +ALL FINE UPLOADER SOFTWARE PROVIDED HEREUNDER IS PROVIDED +“AS IS” . Widen expressly warrants that it is the owner or licensor of Fine Uploader +Software, including any and all copyrights and trade secrets, and has the right and +authority to enter into this SSA in accordance with the terms herein. EXCEPT AS MAY +BE PROVIDED IN ANOTHER WRITTEN AGREEMENT BETWEEN WIDEN AND +THE CUSTOMER, THE FOREGOING WARRANTY IS EXCLUSIVE OF ALL +OTHER WARRANTIES , whether written, oral, express or implied, INCLUDING BUT +NOT LIMITED TO the implied warranties of merchantability or fitness for a particular +purpose. WIDEN DOES NOT WARRANT that the Fine Uploader Software will meet +Customer’s requirements, or that the operation thereof will be uninterrupted or error-free. + +LIMITATION OF LIABILITY. +NOTWITHSTANDING ANY OTHER TERM OF THIS SSA TO THE CONTRARY, +IN NO EVENT SHALL WIDEN (OR ITS EMPLOYEES, AGENTS, SUPPLIERS AND +LICENSORS) BE LIABLE TO CUSTOMER OR ANY THIRD-PARTY CLAIMING +THROUGH CUSTOMER OR END USER FOR ANY DIRECT, INDIRECT, SPECIAL, +INCIDENTAL, CONSEQUENTIAL, EXEMPLARY, CONTINGENT OR PUNITIVE +DAMAGES HOWSOEVER CAUSED (INCLUDING DAMAGES FOR LOSS +OF REVENUE, PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS +INFORMATION, LOSS OF CAPITAL, INCREASED COSTS OF OPERATION, +LITIGATION COSTS AND THE LIKE), WHETHER BASED UPON A CLAIM OR +ACTION IN CONTRACT, TORT (INCLUDING NEGLIGENCE), OR ANY OTHER +LEGAL OR EQUITABLE THEORY, IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE FILEUPLOADER SOFTWARE PROVIDED BY WIDEN +TO CUSTOMER, REGARDLESS OF WHETHER WIDEN HAS BEEN ADVISED +OF THE POSSIBILITY OF SUCH DAMAGES OR SUCH DAMAGES WERE +REASONABLY FORESEEABLE. + +IN NO EVENT SHALL WIDEN’S LIABILITY TO CUSTOMER, WHETHER IN +CONTRACT, TORT (INCLUDING NEGLIGENCE), BREACH OF WARRANTY +OR PURSUANT TO ANY OTHER LEGAL OR EQUITABLE THEORY, EXCEED +THE FEES PAID BY CUSTOMER TO WIDEN PURSUANT TO THIS SSA DURING +THE TWELVE (12) MONTH PERIOD IMMEDIATELY PRIOR TO WIDEN’S +RECEIPT OF CUSTOMER’S WRITTEN CLAIM. CUSTOMER ACKNOWLEDGES +AND AGREES THAT WIDEN HAS ENTERED INTO THIS SSA IN RELIANCE +UPON THE DISCLAIMERS OF WARRANTY AND THE LIMITATIONS OF +LIABILITY SET FORTH HEREIN, THAT THE SAME REFLECT AN ALLOCATION +OF RISK BETWEEN THE PARTIES (INCLUDING THE RISK THAT A +CONTRACT REMEDY MAY FAIL OF ITS ESSENTIAL PURPOSE AND CAUSE +CONSEQUENTIAL LOSS), AND THAT THE SAME FORM AN ESSENTIAL BASIS +OF THE BARGAIN BETWEEN THE PARTIES. +This provision shall survive the termination of this SSA. + +Trademark Rights and Notices. +Customer recognizes and acknowledges Widen’s ownership and title to the Fine +Uploader trademark, and to Widen’s copyrights, patents, trademarks, trade secrets, and +any other intellectual property and proprietary rights of any kind in any jurisdiction +(collectively the “ Widen Intellectual Property Rights ”) embodied in Fine Uploader +Software or on Widen’s website. Nothing in this SSA shall be interpreted to assign +or to grant exclusive rights to Customer of any of Widen Intellectual Property Rights. +Customer hereby agrees not to use the Fine Uploader trademark or Widen’s trade names +in Customer’s corporate title or name, or for its products or services. Neither Party will +engage in any action associated with the other’s intellectual property rights that adversely +affects the good name or goodwill associated with those intellectual property rights. +Customer agrees not to contest or take any action in opposition to the Fine Uploader +trademark or to attempt to register any mark substantially similar to Fine Uploader +trademark. This provision shall survive the termination of this SSA. + +Mutual Confidentiality. +A Party (the “ Discloser ”) may disclose to the other Party (the “ Recipient ”) certain +valuable confidential and proprietary information (“ Confidential Information ”) relating +to the Discloser’s business including without limitation technical data, trade secrets +or unpublished know-how, research and product plans, products and product designs, +inventions, patent applications, copyrighted and unpublished works, financial or other +business information, marketing plans, customer lists, competitive analysis, and tactical +and strategic business objectives. Discloser’s Confidential Information shall be identified +by a prominent mark or accompanying notice that it is “confidential” or “proprietary”, +or shall be identified as Confidential Information in a written notice within thirty (30) +days of its disclosure. Recipient agrees and promises not to disclose said Confidential +Information to any third party who has not also executed a similar confidentiality +agreement with Discloser, unless Discloser intentionally discloses said Confidential +Information to the public or authorizes Recipient to do so in writing as specified in +this SSA. Recipient further agrees to take all reasonable precautions to prevent any +unauthorized disclosure of Discloser’s Confidential Information. Discloser’s Confidential +Information shall no longer be confidential if (a) it is already known to Recipient, as +evidenced by a writing dated prior to the date of disclosure; or (b) it is or becomes +generally known to the public at large through no wrongful act or other involvement +of the Recipient; or (c) it is received from an unaffiliated third party without either an +obligation of nondisclosure or breach of an obligation of confidentiality or nondisclosure; +or (d) it is independently developed by the Recipient or by third parties without any +access whatsoever to the Discloser’s Confidential Information; or (e) it is required to be +disclosed by a court of competent jurisdiction or applicable law, following notice and an +opportunity for Discloser to defend, limit or protect such disclosure. This provision shall +survive the termination of this SSA. + +No Agency. +The Parties are independent contractors. Neither Party is an employee, agent, joint +venturer or legal representative of the other Party for any purpose. Neither Party shall +have the authority to enter into any legal or equitable obligation for the other Party. +Under no circumstances may either Party hold itself out to have agency authority for +the other Party. The Parties agree not to make false or misleading statements, claims or +representations about the other Party, its products or the relationship between the Parties. + +Notices. +All notices required or permitted under this SSA shall be in writing and shall be deemed +received when confirmed by recipient. In each case, such notice shall be provided to the +email address or other address as the Parties may later designate. + +Severability. +If the application of any provision or provisions of this SSA to any particular set of +facts or circumstances is held to be invalid or unenforceable by a court of competent +jurisdiction, the validity of said provision or provisions to any other particular set of facts +or circumstances shall not, in any way, be affected. Such provision or provisions shall +be reformed without further action by the Parties to the extent necessary to make such +provision or provisions enforceable when applied to that set of facts or circumstances. + +Amendment and Waiver. +This SSA may not be modified or amended except in a writing signed by a duly +authorized representative of each Party. The waiver by either Party of any of its rights or +remedies hereunder shall not be deemed a waiver of such rights or remedies in the future +unless such waiver is in writing and signed by an authorized officer of such Party. Such a +waiver shall be limited specifically to the extent set forth in said writing. + +Assignment. +Neither Party may assign this SSA or any right or obligation hereunder, without the other +Party’s prior written consent, which shall not be unreasonably withheld. However, either +Party may assign this SSA in the event of a merger or consolidation or the purchase of all +or substantially all of its assets. This SSA will be binding upon and inure to the benefit of +the permitted successors and assigns of each Party. + +Governing Law and Venue. +The validity, interpretation and enforcement of this SSA shall be governed by and +construed according to the laws of the State of Wisconsin, U.S.A., without reference +to its conflicts of laws doctrine. The Parties irrevocably submit to venue and exclusive +personal jurisdiction in the applicable courts of Dane County, Wisconsin, for any dispute +regarding the subject matter of this SSA including any and all theories of recovery, and +waives all objections to jurisdiction and venue of such courts. Customer and Widen +waive any right to a jury trial regarding any dispute between the Parties. This provision +shall survive the termination of this SSA. + +General. +This SSA constitutes the exclusive terms and conditions with respect to the subject +matter hereof. This SSA represents the final, complete and exclusive statement of the +agreement between the Parties with respect to subject matter hereof and all prior written +agreements and all prior and contemporaneous oral agreements with respect to the +subject matter hereof are merged herein. The Parties both state that it is their intention to +resolve disputes between them concerning this SSA directly in good faith negotiations. +Notwithstanding the foregoing, nothing in this section shall prevent either Party from +applying for and obtaining from a court a temporary restraining order and/or other +injunctive relief. This provision shall survive the termination of this SSA. + +Maintenance and Support + +Engagement of Support Services. +Upon payment by Customer of Fees for Fine Uploader Support as specified at +www.fineuploader.com, Widen shall provide Fine Uploader Support as described in +this SSA to Customer for specified Fine Uploader Software and for the Term identified +herein. + +Software Versioning. +Fine Uploader Software is identified by a version number using the following format: +[major release].[minor release].[patch level]. A “ Release ” is a vehicle for delivering +major and minor feature development and enhancements to existing features in Fine +Uploader Software. A “ Patch ” is a vehicle for delivering enhancements to existing +features and to correct defects. New Patches incorporate all applicable defect corrections +made in prior Patches. New Releases incorporate all applicable defect corrections made +in prior Releases and Patches. + +Eligibility for Support. +Fine Uploader Software is eligible for Fine Uploader Support for a period of twelve (12) +months from the Effective Date. + +Enhancements and Upgrades. +During the Term of Fine Uploader Support, Widen shall provide to Customer, free of +additional charge, all Releases and Patches to the Fine Uploader Software that it makes +generally available. Customer is responsible for installing and testing enhancements and +upgrades. + +Exclusions from Support Services. +Widen shall have no obligation to support Fine Uploader Software: (i) that has been +altered or modified without written authorization by Widen; (ii) that is not installed +on supported systems in accordance with Fine Uploader Software documentation; (iii) +that is experiencing problems caused by Customer’s negligence, misuse, or hardware +malfunction; or (iv) that is being used inconsistent with Fine Uploader Software +documentation. Fine Uploader Support does not include information or assistance +on technical issues related to the debugging, installation, administration, and use of +Customer’s computer systems and enabling technologies including, but not limited to, +databases, computer networks, communications, hardware, hard disks, networks, and +printers. + +Confidentiality of Customer Data. +Widen will not copy or distribute Customer data while providing Fine Uploader Support. + +Limited Warranty for Fine Uploader Support. +Widen warrants that Fine Uploader Support will be performed with the same degree of +skill and professionalism as is demonstrated by like professionals performing services of +a similar nature, and in accordance with generally accepted industry standards, practices, +and principles applicable to such support services. + +Customer Responsibilities. +Customer shall provide reasonable cooperation and full information to Widen with +respect to Widen’s furnishing of Fine Uploader Support under this SSA. + +General Support. +Customer shall submit issues or questions to the Fine Uploader online community forum +as a single issue or question. Widen will respond to the issue or question via the online +community forum administered by Widen. diff --git a/scripts/fineuploader/continue.gif b/scripts/fineuploader/continue.gif new file mode 100644 index 000000000..303b7fbdc Binary files /dev/null and b/scripts/fineuploader/continue.gif differ diff --git a/scripts/fineuploader/edit.gif b/scripts/fineuploader/edit.gif new file mode 100644 index 000000000..403e7c67d Binary files /dev/null and b/scripts/fineuploader/edit.gif differ diff --git a/scripts/fineuploader/fine-uploader-gallery.css b/scripts/fineuploader/fine-uploader-gallery.css new file mode 100644 index 000000000..21344d72b --- /dev/null +++ b/scripts/fineuploader/fine-uploader-gallery.css @@ -0,0 +1,487 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +/* --------------------------------------- +/* Fine Uploader Gallery View Styles +/* --------------------------------------- + + +/* Buttons +------------------------------------------ */ +.qq-gallery .qq-btn +{ + float: right; + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} + +/* Upload Button +------------------------------------------ */ +.qq-gallery .qq-upload-button { + display: inline; + width: 105px; + padding: 7px 10px; + float: left; + text-align: center; + background: #00ABC7; + color: #FFFFFF; + border-radius: 2px; + border: 1px solid #37B7CC; + box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, + 1px 0 1px rgba(255, 255, 255, 0.07) inset, + 0 1px 0 rgba(0, 0, 0, 0.36), + 0 -2px 12px rgba(0, 0, 0, 0.08) inset +} +.qq-gallery .qq-upload-button-hover { + background: #33B6CC; +} +.qq-gallery .qq-upload-button-focus { + outline: 1px dotted #000000; +} + + +/* Drop Zone +------------------------------------------ */ +.qq-gallery.qq-uploader { + position: relative; + min-height: 200px; + max-height: 490px; + overflow-y: hidden; + width: inherit; + border-radius: 6px; + border: 1px dashed #CCCCCC; + background-color: #FAFAFA; + padding: 20px; +} +.qq-gallery.qq-uploader:before { + content: attr(qq-drop-area-text) " "; + position: absolute; + font-size: 200%; + left: 0; + width: 100%; + text-align: center; + top: 45%; + opacity: 0.25; + filter: alpha(opacity=25); +} +.qq-gallery .qq-upload-drop-area, .qq-upload-extra-drop-area { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-height: 30px; + z-index: 2; + background: #F9F9F9; + border-radius: 4px; + text-align: center; +} +.qq-gallery .qq-upload-drop-area span { + display: block; + position: absolute; + top: 50%; + width: 100%; + margin-top: -8px; + font-size: 16px; +} +.qq-gallery .qq-upload-extra-drop-area { + position: relative; + margin-top: 50px; + font-size: 16px; + padding-top: 30px; + height: 20px; + min-height: 40px; +} +.qq-gallery .qq-upload-drop-area-active { + background: #FDFDFD; + border-radius: 4px; +} +.qq-gallery .qq-upload-list { + margin: 0; + padding: 10px 0 0; + list-style: none; + max-height: 450px; + overflow-y: auto; + clear: both; + box-shadow: none; +} + + +/* Uploaded Elements +------------------------------------------ */ +.qq-gallery .qq-upload-list li { + display: inline-block; + position: relative; + max-width: 120px; + margin: 0 25px 25px 0; + padding: 0; + line-height: 16px; + font-size: 13px; + color: #424242; + background-color: #FFFFFF; + border-radius: 2px; + box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.22); + vertical-align: top; + + /* to ensure consistent size of tiles - may need to change if qq-max-size attr on preview img changes */ + height: 186px; +} + +.qq-gallery .qq-upload-spinner, +.qq-gallery .qq-upload-size, +.qq-gallery .qq-upload-retry, +.qq-gallery .qq-upload-failed-text, +.qq-gallery .qq-upload-delete, +.qq-gallery .qq-upload-pause, +.qq-gallery .qq-upload-continue { + display: inline; +} +.qq-gallery .qq-upload-retry:hover, +.qq-gallery .qq-upload-delete:hover, +.qq-gallery .qq-upload-pause:hover, +.qq-gallery .qq-upload-continue:hover { + background-color: transparent; +} +.qq-gallery .qq-upload-delete, +.qq-gallery .qq-upload-pause, +.qq-gallery .qq-upload-continue, +.qq-gallery .qq-upload-cancel { + cursor: pointer; +} +.qq-gallery .qq-upload-delete, +.qq-gallery .qq-upload-pause, +.qq-gallery .qq-upload-continue { + border:none; + background: none; + color: #00A0BA; + font-size: 12px; + padding: 0; +} +/* to ensure consistent size of tiles - only display status text before auto-retry or after failure */ +.qq-gallery .qq-upload-status-text { + color: #333333; + font-size: 12px; + padding-left: 3px; + padding-top: 2px; + width: inherit; + display: none; + width: 108px; +} +.qq-gallery .qq-upload-fail .qq-upload-status-text { + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; + display: block; +} +.qq-gallery .qq-upload-retrying .qq-upload-status-text { + display: inline-block; +} +.qq-gallery .qq-upload-retrying .qq-progress-bar-container { + display: none; +} + +.qq-gallery .qq-upload-cancel { + background-color: #525252; + color: #F7F7F7; + font-weight: bold; + font-family: Arial, Helvetica, sans-serif; + border-radius: 12px; + border: none; + height: 22px; + width: 22px; + padding: 4px; + position: absolute; + right: -5px; + top: -6px; + margin: 0; + line-height: 17px; +} +.qq-gallery .qq-upload-cancel:hover { + background-color: #525252; +} +.qq-gallery .qq-upload-retry { + cursor: pointer; + position: absolute; + top: 30px; + left: 50%; + margin-left: -31px; + box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, + 1px 0 1px rgba(255, 255, 255, 0.07) inset, + 0 4px 4px rgba(0, 0, 0, 0.5), + 0 -2px 12px rgba(0, 0, 0, 0.08) inset; + padding: 3px 4px; + border: 1px solid #d2ddc7; + border-radius: 2px; + color: inherit; + background-color: #EBF6E0; + z-index: 1; +} +.qq-gallery .qq-upload-retry:hover { + background-color: #f7ffec; +} + +.qq-gallery .qq-file-info { + padding: 10px 6px 4px; + margin-top: -3px; + border-radius: 0 0 2px 2px; + text-align: left; + overflow: hidden; +} + +.qq-gallery .qq-file-info .qq-file-name { + position: relative; +} + +.qq-gallery .qq-upload-file { + display: block; + margin-right: 0; + margin-bottom: 3px; + width: auto; + + /* to ensure consistent size of tiles - constrain text to single line */ + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} +.qq-gallery .qq-upload-spinner { + display: inline-block; + background: url("loading.gif"); + position: absolute; + left: 50%; + margin-left: -7px; + top: 53px; + width: 15px; + height: 15px; + vertical-align: text-bottom; +} +.qq-gallery .qq-drop-processing { + display: block; +} +.qq-gallery .qq-drop-processing-spinner { + display: inline-block; + background: url("processing.gif"); + width: 24px; + height: 24px; + vertical-align: text-bottom; +} +.qq-gallery .qq-upload-failed-text { + display: none; + font-style: italic; + font-weight: bold; +} +.qq-gallery .qq-upload-failed-icon { + display:none; + width:15px; + height:15px; + vertical-align:text-bottom; +} +.qq-gallery .qq-upload-fail .qq-upload-failed-text { + display: inline; +} +.qq-gallery .qq-upload-retrying .qq-upload-failed-text { + display: inline; +} +.qq-gallery .qq-upload-list li.qq-upload-success { + background-color: #F2F7ED; +} +.qq-gallery .qq-upload-list li.qq-upload-fail { + background-color: #F5EDED; + box-shadow: 0 0 1px 0 red; + border: 0; +} +.qq-gallery .qq-progress-bar { + display: block; + background: #00abc7; + width: 0%; + height: 15px; + border-radius: 6px; + margin-bottom: 3px; +} + +.qq-gallery .qq-total-progress-bar { + height: 25px; + border-radius: 9px; +} + +.qq-gallery .qq-total-progress-bar-container { + margin-left: 9px; + display: inline; + float: right; + width: 500px; +} + +.qq-gallery .qq-upload-size { + float: left; + font-size: 11px; + color: #929292; + margin-bottom: 3px; + margin-right: 0; + display: inline-block; +} + +.qq-gallery INPUT.qq-edit-filename { + position: absolute; + opacity: 0; + filter: alpha(opacity=0); + z-index: -1; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; +} + +.qq-gallery .qq-upload-file.qq-editable { + cursor: pointer; + margin-right: 20px; +} + +.qq-gallery .qq-edit-filename-icon.qq-editable { + display: inline-block; + cursor: pointer; + position: absolute; + right: 0; + top: 0; +} + +.qq-gallery INPUT.qq-edit-filename.qq-editing { + position: static; + height: 28px; + width: 90px; + width: -moz-available; + padding: 0 8px; + margin-bottom: 3px; + border: 1px solid #ccc; + border-radius: 2px; + font-size: 13px; + + opacity: 1; + filter: alpha(opacity=100); + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; +} + +.qq-gallery .qq-edit-filename-icon { + display: none; + background: url("edit.gif"); + width: 15px; + height: 15px; + vertical-align: text-bottom; +} +.qq-gallery .qq-delete-icon { + background: url("trash.gif"); + width: 15px; + height: 15px; + vertical-align: sub; + display: inline-block; +} +.qq-gallery .qq-retry-icon { + background: url("retry.gif"); + width: 15px; + height: 15px; + vertical-align: sub; + display: inline-block; + float: none; +} +.qq-gallery .qq-continue-icon { + background: url("continue.gif"); + width: 15px; + height: 15px; + vertical-align: sub; + display: inline-block; +} +.qq-gallery .qq-pause-icon { + background: url("pause.gif"); + width: 15px; + height: 15px; + vertical-align: sub; + display: inline-block; +} + +.qq-gallery .qq-hide { + display: none; +} + + +/* Thumbnail +------------------------------------------ */ +.qq-gallery .qq-in-progress .qq-thumbnail-wrapper { + /* makes the spinner on top of the thumbnail more visible */ + opacity: 0.5; + filter: alpha(opacity=50); +} +.qq-gallery .qq-thumbnail-wrapper { + overflow: hidden; + position: relative; + + /* to ensure consistent size of tiles - should match qq-max-size attribute value on qq-thumbnail-selector IMG element */ + height: 120px; + width: 120px; +} +.qq-gallery .qq-thumbnail-selector { + border-radius: 2px 2px 0 0; + bottom: 0; + + /* we will override this in the :root thumbnail selector (to help center the preview) for everything other than IE8 */ + top: 0; + + /* center the thumb horizontally in the tile */ + margin:auto; + display: block; +} + +/* hack to ensure we don't try to center preview in IE8, since -ms-filter doesn't mimic translateY as expected in all cases */ +:root *> .qq-gallery .qq-thumbnail-selector { + /* vertically center preview image on tile */ + position: relative; + top: 50%; + transform: translateY(-50%); + -moz-transform: translateY(-50%); + -ms-transform: translateY(-50%); + -webkit-transform: translateY(-50%); +} + +/* element styles */ +.qq-gallery.qq-uploader DIALOG { + display: none; +} + +.qq-gallery.qq-uploader DIALOG[open] { + display: block; +} + +.qq-gallery.qq-uploader DIALOG { + display: none; +} + +.qq-gallery.qq-uploader DIALOG[open] { + display: block; +} + +.qq-gallery.qq-uploader DIALOG .qq-dialog-buttons { + text-align: center; + padding-top: 10px; +} + +.qq-gallery.qq-uploader DIALOG .qq-dialog-buttons BUTTON { + margin-left: 5px; + margin-right: 5px; +} + +.qq-gallery.qq-uploader DIALOG .qq-dialog-message-selector { + padding-bottom: 10px; +} + +.qq-gallery .qq-uploader DIALOG::backdrop { + background-color: rgba(0, 0, 0, 0.7); +} +/*! 2016-03-16 */ diff --git a/scripts/fineuploader/fine-uploader-gallery.min.css b/scripts/fineuploader/fine-uploader-gallery.min.css new file mode 100644 index 000000000..3c02c97ed --- /dev/null +++ b/scripts/fineuploader/fine-uploader-gallery.min.css @@ -0,0 +1,19 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +/*! fine-uploader 2016-03-16 */ + +.qq-gallery .qq-btn{float:right;border:0;padding:0;margin:0;box-shadow:none}.qq-gallery .qq-upload-button{display:inline;width:105px;padding:7px 10px;float:left;text-align:center;background:#00ABC7;color:#FFF;border-radius:2px;border:1px solid #37B7CC;box-shadow:0 1px 1px rgba(255,255,255,.37) inset,1px 0 1px rgba(255,255,255,.07) inset,0 1px 0 rgba(0,0,0,.36),0 -2px 12px rgba(0,0,0,.08) inset}.qq-gallery .qq-upload-button-hover{background:#33B6CC}.qq-gallery .qq-upload-button-focus{outline:1px dotted #000}.qq-gallery.qq-uploader{position:relative;min-height:200px;max-height:490px;overflow-y:hidden;width:inherit;border-radius:6px;border:1px dashed #CCC;background-color:#FAFAFA;padding:20px}.qq-gallery.qq-uploader:before{content:attr(qq-drop-area-text) " ";position:absolute;font-size:200%;left:0;width:100%;text-align:center;top:45%;opacity:.25;filter:alpha(opacity=25)}.qq-gallery .qq-upload-drop-area,.qq-upload-extra-drop-area{position:absolute;top:0;left:0;width:100%;height:100%;min-height:30px;z-index:2;background:#F9F9F9;border-radius:4px;text-align:center}.qq-gallery .qq-upload-drop-area span{display:block;position:absolute;top:50%;width:100%;margin-top:-8px;font-size:16px}.qq-gallery .qq-upload-extra-drop-area{position:relative;margin-top:50px;font-size:16px;padding-top:30px;height:20px;min-height:40px}.qq-gallery .qq-upload-drop-area-active{background:#FDFDFD;border-radius:4px}.qq-gallery .qq-upload-list{margin:0;padding:10px 0 0;list-style:none;max-height:450px;overflow-y:auto;clear:both;box-shadow:none}.qq-gallery .qq-upload-list li{display:inline-block;position:relative;max-width:120px;margin:0 25px 25px 0;padding:0;line-height:16px;font-size:13px;color:#424242;background-color:#FFF;border-radius:2px;box-shadow:0 1px 1px 0 rgba(0,0,0,.22);vertical-align:top;height:186px}.qq-gallery .qq-upload-spinner,.qq-gallery .qq-upload-size,.qq-gallery .qq-upload-retry,.qq-gallery .qq-upload-failed-text,.qq-gallery .qq-upload-delete,.qq-gallery .qq-upload-pause,.qq-gallery .qq-upload-continue{display:inline}.qq-gallery .qq-upload-retry:hover,.qq-gallery .qq-upload-delete:hover,.qq-gallery .qq-upload-pause:hover,.qq-gallery .qq-upload-continue:hover{background-color:transparent}.qq-gallery .qq-upload-delete,.qq-gallery .qq-upload-pause,.qq-gallery .qq-upload-continue,.qq-gallery .qq-upload-cancel{cursor:pointer}.qq-gallery .qq-upload-delete,.qq-gallery .qq-upload-pause,.qq-gallery .qq-upload-continue{border:0;background:0;color:#00A0BA;font-size:12px;padding:0}.qq-gallery .qq-upload-status-text{color:#333;font-size:12px;padding-left:3px;padding-top:2px;width:inherit;display:none;width:108px}.qq-gallery .qq-upload-fail .qq-upload-status-text{text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden;display:block}.qq-gallery .qq-upload-retrying .qq-upload-status-text{display:inline-block}.qq-gallery .qq-upload-retrying .qq-progress-bar-container{display:none}.qq-gallery .qq-upload-cancel{background-color:#525252;color:#F7F7F7;font-weight:700;font-family:Arial,Helvetica,sans-serif;border-radius:12px;border:0;height:22px;width:22px;padding:4px;position:absolute;right:-5px;top:-6px;margin:0;line-height:17px}.qq-gallery .qq-upload-cancel:hover{background-color:#525252}.qq-gallery .qq-upload-retry{cursor:pointer;position:absolute;top:30px;left:50%;margin-left:-31px;box-shadow:0 1px 1px rgba(255,255,255,.37) inset,1px 0 1px rgba(255,255,255,.07) inset,0 4px 4px rgba(0,0,0,.5),0 -2px 12px rgba(0,0,0,.08) inset;padding:3px 4px;border:1px solid #d2ddc7;border-radius:2px;color:inherit;background-color:#EBF6E0;z-index:1}.qq-gallery .qq-upload-retry:hover{background-color:#f7ffec}.qq-gallery .qq-file-info{padding:10px 6px 4px;margin-top:-3px;border-radius:0 0 2px 2px;text-align:left;overflow:hidden}.qq-gallery .qq-file-info .qq-file-name{position:relative}.qq-gallery .qq-upload-file{display:block;margin-right:0;margin-bottom:3px;width:auto;text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden}.qq-gallery .qq-upload-spinner{display:inline-block;background:url(loading.gif);position:absolute;left:50%;margin-left:-7px;top:53px;width:15px;height:15px;vertical-align:text-bottom}.qq-gallery .qq-drop-processing{display:block}.qq-gallery .qq-drop-processing-spinner{display:inline-block;background:url(processing.gif);width:24px;height:24px;vertical-align:text-bottom}.qq-gallery .qq-upload-failed-text{display:none;font-style:italic;font-weight:700}.qq-gallery .qq-upload-failed-icon{display:none;width:15px;height:15px;vertical-align:text-bottom}.qq-gallery .qq-upload-fail .qq-upload-failed-text{display:inline}.qq-gallery .qq-upload-retrying .qq-upload-failed-text{display:inline}.qq-gallery .qq-upload-list li.qq-upload-success{background-color:#F2F7ED}.qq-gallery .qq-upload-list li.qq-upload-fail{background-color:#F5EDED;box-shadow:0 0 1px 0 red;border:0}.qq-gallery .qq-progress-bar{display:block;background:#00abc7;width:0;height:15px;border-radius:6px;margin-bottom:3px}.qq-gallery .qq-total-progress-bar{height:25px;border-radius:9px}.qq-gallery .qq-total-progress-bar-container{margin-left:9px;display:inline;float:right;width:500px}.qq-gallery .qq-upload-size{float:left;font-size:11px;color:#929292;margin-bottom:3px;margin-right:0;display:inline-block}.qq-gallery INPUT.qq-edit-filename{position:absolute;opacity:0;filter:alpha(opacity=0);z-index:-1;-ms-filter:"alpha(Opacity=0)"}.qq-gallery .qq-upload-file.qq-editable{cursor:pointer;margin-right:20px}.qq-gallery .qq-edit-filename-icon.qq-editable{display:inline-block;cursor:pointer;position:absolute;right:0;top:0}.qq-gallery INPUT.qq-edit-filename.qq-editing{position:static;height:28px;width:90px;width:-moz-available;padding:0 8px;margin-bottom:3px;border:1px solid #ccc;border-radius:2px;font-size:13px;opacity:1;filter:alpha(opacity=100);-ms-filter:"alpha(Opacity=100)"}.qq-gallery .qq-edit-filename-icon{display:none;background:url(edit.gif);width:15px;height:15px;vertical-align:text-bottom}.qq-gallery .qq-delete-icon{background:url(trash.gif);width:15px;height:15px;vertical-align:sub;display:inline-block}.qq-gallery .qq-retry-icon{background:url(retry.gif);width:15px;height:15px;vertical-align:sub;display:inline-block;float:none}.qq-gallery .qq-continue-icon{background:url(continue.gif);width:15px;height:15px;vertical-align:sub;display:inline-block}.qq-gallery .qq-pause-icon{background:url(pause.gif);width:15px;height:15px;vertical-align:sub;display:inline-block}.qq-gallery .qq-hide{display:none}.qq-gallery .qq-in-progress .qq-thumbnail-wrapper{opacity:.5;filter:alpha(opacity=50)}.qq-gallery .qq-thumbnail-wrapper{overflow:hidden;position:relative;height:120px;width:120px}.qq-gallery .qq-thumbnail-selector{border-radius:2px 2px 0 0;bottom:0;top:0;margin:auto;display:block}:root *> .qq-gallery .qq-thumbnail-selector{position:relative;top:50%;transform:translateY(-50%);-moz-transform:translateY(-50%);-ms-transform:translateY(-50%);-webkit-transform:translateY(-50%)}.qq-gallery.qq-uploader DIALOG{display:none}.qq-gallery.qq-uploader DIALOG[open]{display:block}.qq-gallery.qq-uploader DIALOG{display:none}.qq-gallery.qq-uploader DIALOG[open]{display:block}.qq-gallery.qq-uploader DIALOG .qq-dialog-buttons{text-align:center;padding-top:10px}.qq-gallery.qq-uploader DIALOG .qq-dialog-buttons BUTTON{margin-left:5px;margin-right:5px}.qq-gallery.qq-uploader DIALOG .qq-dialog-message-selector{padding-bottom:10px}.qq-gallery .qq-uploader DIALOG::backdrop{background-color:rgba(0,0,0,.7)} +/*! 2016-03-16 */ diff --git a/scripts/fineuploader/fine-uploader-new.css b/scripts/fineuploader/fine-uploader-new.css new file mode 100644 index 000000000..bed115f08 --- /dev/null +++ b/scripts/fineuploader/fine-uploader-new.css @@ -0,0 +1,370 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +/* --------------------------------------- +/* Fine Uploader Styles +/* --------------------------------------- + +/* Buttons +------------------------------------------ */ +.qq-btn +{ + box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, + 1px 0 1px rgba(255, 255, 255, 0.07) inset, + 0 1px 0 rgba(0, 0, 0, 0.36), + 0 -2px 12px rgba(0, 0, 0, 0.08) inset; + padding: 3px 4px; + border: 1px solid #CCCCCC; + border-radius: 2px; + color: inherit; + background-color: #FFFFFF; +} +.qq-upload-delete, .qq-upload-pause, .qq-upload-continue { + display: inline; +} +.qq-upload-delete +{ + background-color: #e65c47; + color: #FAFAFA; + border-color: #dc523d; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55); +} +.qq-upload-delete:hover { + background-color: #f56b56; + } +.qq-upload-cancel +{ + background-color: #F5D7D7; + border-color: #e6c8c8; +} +.qq-upload-cancel:hover { + background-color: #ffe1e1; +} +.qq-upload-retry +{ + background-color: #EBF6E0; + border-color: #d2ddc7; +} +.qq-upload-retry:hover { + background-color: #f7ffec; +} +.qq-upload-pause, .qq-upload-continue { + background-color: #00ABC7; + color: #FAFAFA; + border-color: #2dadc2; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.55); +} +.qq-upload-pause:hover, .qq-upload-continue:hover { + background-color: #0fbad6; +} + +/* Upload Button +------------------------------------------ */ +.qq-upload-button { + display: inline; + width: 105px; + margin-bottom: 10px; + padding: 7px 10px; + text-align: center; + float: left; + background: #00ABC7; + color: #FFFFFF; + border-radius: 2px; + border: 1px solid #2dadc2; + box-shadow: 0 1px 1px rgba(255, 255, 255, 0.37) inset, + 1px 0 1px rgba(255, 255, 255, 0.07) inset, + 0 1px 0 rgba(0, 0, 0, 0.36), + 0 -2px 12px rgba(0, 0, 0, 0.08) inset; +} +.qq-upload-button-hover { + background: #33B6CC; +} +.qq-upload-button-focus { + outline: 1px dotted #000000; +} + + +/* Drop Zone +------------------------------------------ */ +.qq-uploader { + position: relative; + min-height: 200px; + max-height: 490px; + overflow-y: hidden; + width: inherit; + border-radius: 6px; + background-color: #FDFDFD; + border: 1px dashed #CCCCCC; + padding: 20px; +} +.qq-uploader:before { + content: attr(qq-drop-area-text) " "; + position: absolute; + font-size: 200%; + left: 0; + width: 100%; + text-align: center; + top: 45%; + opacity: 0.25; +} +.qq-upload-drop-area, .qq-upload-extra-drop-area { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-height: 30px; + z-index: 2; + background: #F9F9F9; + border-radius: 4px; + border: 1px dashed #CCCCCC; + text-align: center; +} +.qq-upload-drop-area span { + display: block; + position: absolute; + top: 50%; + width: 100%; + margin-top: -8px; + font-size: 16px; +} +.qq-upload-extra-drop-area { + position: relative; + margin-top: 50px; + font-size: 16px; + padding-top: 30px; + height: 20px; + min-height: 40px; +} +.qq-upload-drop-area-active { + background: #FDFDFD; + border-radius: 4px; + border: 1px dashed #CCCCCC; +} +.qq-upload-list { + margin: 0; + padding: 0; + list-style: none; + max-height: 450px; + overflow-y: auto; + box-shadow: 0px 1px 0px rgba(15, 15, 50, 0.14); + clear: both; +} + + +/* Uploaded Elements +------------------------------------------ */ +.qq-upload-list li { + margin: 0; + padding: 9px; + line-height: 15px; + font-size: 16px; + color: #424242; + background-color: #F6F6F6; + border-top: 1px solid #FFFFFF; + border-bottom: 1px solid #DDDDDD; +} +.qq-upload-list li:first-child { + border-top: none; +} +.qq-upload-list li:last-child { + border-bottom: none; +} + +.qq-upload-file, .qq-upload-spinner, .qq-upload-size, +.qq-upload-cancel, .qq-upload-retry, .qq-upload-failed-text, +.qq-upload-delete, .qq-upload-pause, .qq-upload-continue { + margin-right: 12px; + display: inline; +} +.qq-upload-file { + vertical-align: middle; + display: inline-block; + width: 300px; + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; + height: 18px; +} +.qq-upload-spinner { + display: inline-block; + background: url("loading.gif"); + width: 15px; + height: 15px; + vertical-align: text-bottom; +} +.qq-drop-processing { + display: block; +} +.qq-drop-processing-spinner { + display: inline-block; + background: url("processing.gif"); + width: 24px; + height: 24px; + vertical-align: text-bottom; +} +.qq-upload-size, .qq-upload-cancel, .qq-upload-retry, +.qq-upload-delete, .qq-upload-pause, .qq-upload-continue { + font-size: 12px; + font-weight: normal; + cursor: pointer; + vertical-align: middle; +} +.qq-upload-status-text { + font-size: 14px; + font-weight: bold; + display: block; +} +.qq-upload-failed-text { + display: none; + font-style: italic; + font-weight: bold; +} +.qq-upload-failed-icon { + display:none; + width:15px; + height:15px; + vertical-align:text-bottom; +} +.qq-upload-fail .qq-upload-failed-text { + display: inline; +} +.qq-upload-retrying .qq-upload-failed-text { + display: inline; +} +.qq-upload-list li.qq-upload-success { + background-color: #EBF6E0; + color: #424242; + border-bottom: 1px solid #D3DED1; + border-top: 1px solid #F7FFF5; +} +.qq-upload-list li.qq-upload-fail { + background-color: #F5D7D7; + color: #424242; + border-bottom: 1px solid #DECACA; + border-top: 1px solid #FCE6E6; +} +.qq-progress-bar { + display: block; + display: block; + background: #00abc7; + width: 0%; + height: 15px; + border-radius: 6px; + margin-bottom: 3px; +} + +.qq-total-progress-bar { + height: 25px; + border-radius: 9px; +} + +.qq-total-progress-bar-container { + margin-left: 9px; + display: inline; + float: right; + width: 500px; +} + +INPUT.qq-edit-filename { + position: absolute; + opacity: 0; + filter: alpha(opacity=0); + z-index: -1; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; +} + +.qq-upload-file.qq-editable { + cursor: pointer; + margin-right: 4px; +} + +.qq-edit-filename-icon.qq-editable { + display: inline-block; + cursor: pointer; +} + +INPUT.qq-edit-filename.qq-editing { + position: static; + height: 28px; + padding: 0 8px; + margin-right: 10px; + margin-bottom: -5px; + border: 1px solid #ccc; + border-radius: 2px; + font-size: 16px; + + opacity: 1; + filter: alpha(opacity=100); + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; +} + +.qq-edit-filename-icon { + display: none; + background: url("edit.gif"); + width: 15px; + height: 15px; + vertical-align: text-bottom; + margin-right: 16px; +} + +.qq-hide { + display: none; +} + + +/* Thumbnail +------------------------------------------ */ +.qq-thumbnail-selector { + vertical-align: middle; + margin-right: 12px; +} + + +/* element styles */ +.qq-uploader DIALOG { + display: none; +} + +.qq-uploader DIALOG[open] { + display: block; +} + +.qq-uploader DIALOG { + display: none; +} + +.qq-uploader DIALOG[open] { + display: block; +} + +.qq-uploader DIALOG .qq-dialog-buttons { + text-align: center; + padding-top: 10px; +} + +.qq-uploader DIALOG .qq-dialog-buttons BUTTON { + margin-left: 5px; + margin-right: 5px; +} + +.qq-uploader DIALOG .qq-dialog-message-selector { + padding-bottom: 10px; +} + +.qq-uploader DIALOG::backdrop { + background-color: rgba(0, 0, 0, 0.7); +} +/*! 2016-03-16 */ diff --git a/scripts/fineuploader/fine-uploader-new.min.css b/scripts/fineuploader/fine-uploader-new.min.css new file mode 100644 index 000000000..a2b919bb4 --- /dev/null +++ b/scripts/fineuploader/fine-uploader-new.min.css @@ -0,0 +1,19 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +/*! fine-uploader 2016-03-16 */ + +.qq-btn{box-shadow:0 1px 1px rgba(255,255,255,.37) inset,1px 0 1px rgba(255,255,255,.07) inset,0 1px 0 rgba(0,0,0,.36),0 -2px 12px rgba(0,0,0,.08) inset;padding:3px 4px;border:1px solid #CCC;border-radius:2px;color:inherit;background-color:#FFF}.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{display:inline}.qq-upload-delete{background-color:#e65c47;color:#FAFAFA;border-color:#dc523d;text-shadow:0 1px 1px rgba(0,0,0,.55)}.qq-upload-delete:hover{background-color:#f56b56}.qq-upload-cancel{background-color:#F5D7D7;border-color:#e6c8c8}.qq-upload-cancel:hover{background-color:#ffe1e1}.qq-upload-retry{background-color:#EBF6E0;border-color:#d2ddc7}.qq-upload-retry:hover{background-color:#f7ffec}.qq-upload-pause,.qq-upload-continue{background-color:#00ABC7;color:#FAFAFA;border-color:#2dadc2;text-shadow:0 1px 1px rgba(0,0,0,.55)}.qq-upload-pause:hover,.qq-upload-continue:hover{background-color:#0fbad6}.qq-upload-button{display:inline;width:105px;margin-bottom:10px;padding:7px 10px;text-align:center;float:left;background:#00ABC7;color:#FFF;border-radius:2px;border:1px solid #2dadc2;box-shadow:0 1px 1px rgba(255,255,255,.37) inset,1px 0 1px rgba(255,255,255,.07) inset,0 1px 0 rgba(0,0,0,.36),0 -2px 12px rgba(0,0,0,.08) inset}.qq-upload-button-hover{background:#33B6CC}.qq-upload-button-focus{outline:1px dotted #000}.qq-uploader{position:relative;min-height:200px;max-height:490px;overflow-y:hidden;width:inherit;border-radius:6px;background-color:#FDFDFD;border:1px dashed #CCC;padding:20px}.qq-uploader:before{content:attr(qq-drop-area-text) " ";position:absolute;font-size:200%;left:0;width:100%;text-align:center;top:45%;opacity:.25}.qq-upload-drop-area,.qq-upload-extra-drop-area{position:absolute;top:0;left:0;width:100%;height:100%;min-height:30px;z-index:2;background:#F9F9F9;border-radius:4px;border:1px dashed #CCC;text-align:center}.qq-upload-drop-area span{display:block;position:absolute;top:50%;width:100%;margin-top:-8px;font-size:16px}.qq-upload-extra-drop-area{position:relative;margin-top:50px;font-size:16px;padding-top:30px;height:20px;min-height:40px}.qq-upload-drop-area-active{background:#FDFDFD;border-radius:4px;border:1px dashed #CCC}.qq-upload-list{margin:0;padding:0;list-style:none;max-height:450px;overflow-y:auto;box-shadow:0 1px 0 rgba(15,15,50,.14);clear:both}.qq-upload-list li{margin:0;padding:9px;line-height:15px;font-size:16px;color:#424242;background-color:#F6F6F6;border-top:1px solid #FFF;border-bottom:1px solid #DDD}.qq-upload-list li:first-child{border-top:0}.qq-upload-list li:last-child{border-bottom:0}.qq-upload-file,.qq-upload-spinner,.qq-upload-size,.qq-upload-cancel,.qq-upload-retry,.qq-upload-failed-text,.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{margin-right:12px;display:inline}.qq-upload-file{vertical-align:middle;display:inline-block;width:300px;text-overflow:ellipsis;white-space:nowrap;overflow-x:hidden;height:18px}.qq-upload-spinner{display:inline-block;background:url(loading.gif);width:15px;height:15px;vertical-align:text-bottom}.qq-drop-processing{display:block}.qq-drop-processing-spinner{display:inline-block;background:url(processing.gif);width:24px;height:24px;vertical-align:text-bottom}.qq-upload-size,.qq-upload-cancel,.qq-upload-retry,.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{font-size:12px;font-weight:400;cursor:pointer;vertical-align:middle}.qq-upload-status-text{font-size:14px;font-weight:700;display:block}.qq-upload-failed-text{display:none;font-style:italic;font-weight:700}.qq-upload-failed-icon{display:none;width:15px;height:15px;vertical-align:text-bottom}.qq-upload-fail .qq-upload-failed-text{display:inline}.qq-upload-retrying .qq-upload-failed-text{display:inline}.qq-upload-list li.qq-upload-success{background-color:#EBF6E0;color:#424242;border-bottom:1px solid #D3DED1;border-top:1px solid #F7FFF5}.qq-upload-list li.qq-upload-fail{background-color:#F5D7D7;color:#424242;border-bottom:1px solid #DECACA;border-top:1px solid #FCE6E6}.qq-progress-bar{display:block;display:block;background:#00abc7;width:0;height:15px;border-radius:6px;margin-bottom:3px}.qq-total-progress-bar{height:25px;border-radius:9px}.qq-total-progress-bar-container{margin-left:9px;display:inline;float:right;width:500px}INPUT.qq-edit-filename{position:absolute;opacity:0;filter:alpha(opacity=0);z-index:-1;-ms-filter:"alpha(Opacity=0)"}.qq-upload-file.qq-editable{cursor:pointer;margin-right:4px}.qq-edit-filename-icon.qq-editable{display:inline-block;cursor:pointer}INPUT.qq-edit-filename.qq-editing{position:static;height:28px;padding:0 8px;margin-right:10px;margin-bottom:-5px;border:1px solid #ccc;border-radius:2px;font-size:16px;opacity:1;filter:alpha(opacity=100);-ms-filter:"alpha(Opacity=100)"}.qq-edit-filename-icon{display:none;background:url(edit.gif);width:15px;height:15px;vertical-align:text-bottom;margin-right:16px}.qq-hide{display:none}.qq-thumbnail-selector{vertical-align:middle;margin-right:12px}.qq-uploader DIALOG{display:none}.qq-uploader DIALOG[open]{display:block}.qq-uploader DIALOG{display:none}.qq-uploader DIALOG[open]{display:block}.qq-uploader DIALOG .qq-dialog-buttons{text-align:center;padding-top:10px}.qq-uploader DIALOG .qq-dialog-buttons BUTTON{margin-left:5px;margin-right:5px}.qq-uploader DIALOG .qq-dialog-message-selector{padding-bottom:10px}.qq-uploader DIALOG::backdrop{background-color:rgba(0,0,0,.7)} +/*! 2016-03-16 */ diff --git a/scripts/fineuploader/fine-uploader.css b/scripts/fineuploader/fine-uploader.css new file mode 100644 index 000000000..4fd194675 --- /dev/null +++ b/scripts/fineuploader/fine-uploader.css @@ -0,0 +1,241 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +.qq-uploader { + position: relative; + width: 100%; +} +.qq-upload-button { + display: block; + width: 105px; + padding: 7px 0; + text-align: center; + background: #880000; + border-bottom: 1px solid #DDD; + color: #FFF; +} +.qq-upload-button-hover { + background: #CC0000; +} +.qq-upload-button-focus { + outline: 1px dotted #000000; +} +.qq-upload-drop-area, .qq-upload-extra-drop-area { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + min-height: 30px; + z-index: 2; + background: #FF9797; + text-align: center; +} +.qq-upload-drop-area span { + display: block; + position: absolute; + top: 50%; + width: 100%; + margin-top: -8px; + font-size: 16px; +} +.qq-upload-extra-drop-area { + position: relative; + margin-top: 50px; + font-size: 16px; + padding-top: 30px; + height: 20px; + min-height: 40px; +} +.qq-upload-drop-area-active { + background: #FF7171; +} +.qq-upload-list { + margin: 0; + padding: 0; + list-style: none; +} +.qq-upload-list li { + margin: 0; + padding: 9px; + line-height: 15px; + font-size: 16px; + background-color: #FFF0BD; +} +.qq-upload-file, .qq-upload-spinner, .qq-upload-size, +.qq-upload-cancel, .qq-upload-retry, .qq-upload-failed-text, +.qq-upload-delete, .qq-upload-pause, .qq-upload-continue { + margin-right: 12px; + display: inline; +} +.qq-upload-file { +} +.qq-upload-spinner { + display: inline-block; + background: url("loading.gif"); + width: 15px; + height: 15px; + vertical-align: text-bottom; +} +.qq-drop-processing { + display: block; +} +.qq-drop-processing-spinner { + display: inline-block; + background: url("processing.gif"); + width: 24px; + height: 24px; + vertical-align: text-bottom; +} + +.qq-upload-delete, .qq-upload-pause, .qq-upload-continue { + display: inline; +} + +.qq-upload-retry, .qq-upload-delete, .qq-upload-cancel, +.qq-upload-pause, .qq-upload-continue { + color: #000000; +} + +.qq-upload-size, .qq-upload-cancel, .qq-upload-retry, +.qq-upload-delete, .qq-upload-pause, .qq-upload-continue { + font-size: 12px; + font-weight: normal; +} +.qq-upload-failed-text { + display: none; + font-style: italic; + font-weight: bold; +} +.qq-upload-failed-icon { + display:none; + width:15px; + height:15px; + vertical-align:text-bottom; +} +.qq-upload-fail .qq-upload-failed-text { + display: inline; +} +.qq-upload-retrying .qq-upload-failed-text { + display: inline; + color: #D60000; +} +.qq-upload-list li.qq-upload-success { + background-color: #5DA30C; + color: #FFFFFF; +} +.qq-upload-list li.qq-upload-fail { + background-color: #D60000; + color: #FFFFFF; +} +.qq-progress-bar { + display: block; + background: -moz-linear-gradient(top, rgba(30,87,153,1) 0%, rgba(41,137,216,1) 50%, rgba(32,124,202,1) 51%, rgba(125,185,232,1) 100%); /* FF3.6+ */ + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(30,87,153,1)), color-stop(50%,rgba(41,137,216,1)), color-stop(51%,rgba(32,124,202,1)), color-stop(100%,rgba(125,185,232,1))); /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* Chrome10+,Safari5.1+ */ + background: -o-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* Opera 11.10+ */ + background: -ms-linear-gradient(top, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* IE10+ */ + background: linear-gradient(to bottom, rgba(30,87,153,1) 0%,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%); /* W3C */ + width: 0%; + height: 15px; + border-radius: 6px; + margin-bottom: 3px; +} + +.qq-total-progress-bar { + height: 25px; + border-radius: 9px; +} + +.qq-total-progress-bar-container { + margin: 9px; +} + +INPUT.qq-edit-filename { + position: absolute; + opacity: 0; + filter: alpha(opacity=0); + z-index: -1; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; +} + +.qq-upload-file.qq-editable { + cursor: pointer; +} + +.qq-edit-filename-icon.qq-editable { + display: inline-block; + cursor: pointer; +} + +INPUT.qq-edit-filename.qq-editing { + position: static; + margin-top: -5px; + margin-right: 10px; + margin-bottom: -5px; + + opacity: 1; + filter: alpha(opacity=100); + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; +} + +.qq-edit-filename-icon { + display: none; + background: url("edit.gif"); + width: 15px; + height: 15px; + vertical-align: text-bottom; + margin-right: 5px; +} + +.qq-hide { + display: none; +} + +/* element styles */ +.qq-uploader DIALOG { + display: none; +} + +.qq-uploader DIALOG[open] { + display: block; +} + +.qq-uploader DIALOG { + display: none; +} + +.qq-uploader DIALOG[open] { + display: block; +} + +.qq-uploader DIALOG .qq-dialog-buttons { + text-align: center; + padding-top: 10px; +} + +.qq-uploader DIALOG .qq-dialog-buttons BUTTON { + margin-left: 5px; + margin-right: 5px; +} + +.qq-uploader DIALOG .qq-dialog-message-selector { + padding-bottom: 10px; +} + +.qq-uploader DIALOG::backdrop { + background-color: rgba(0, 0, 0, 0.7); +} +/*! 2016-03-16 */ diff --git a/scripts/fineuploader/fine-uploader.min.css b/scripts/fineuploader/fine-uploader.min.css new file mode 100644 index 000000000..3e1ed3f44 --- /dev/null +++ b/scripts/fineuploader/fine-uploader.min.css @@ -0,0 +1,19 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +/*! fine-uploader 2016-03-16 */ + +.qq-uploader{position:relative;width:100%}.qq-upload-button{display:block;width:105px;padding:7px 0;text-align:center;background:#800;border-bottom:1px solid #DDD;color:#FFF}.qq-upload-button-hover{background:#C00}.qq-upload-button-focus{outline:1px dotted #000}.qq-upload-drop-area,.qq-upload-extra-drop-area{position:absolute;top:0;left:0;width:100%;height:100%;min-height:30px;z-index:2;background:#FF9797;text-align:center}.qq-upload-drop-area span{display:block;position:absolute;top:50%;width:100%;margin-top:-8px;font-size:16px}.qq-upload-extra-drop-area{position:relative;margin-top:50px;font-size:16px;padding-top:30px;height:20px;min-height:40px}.qq-upload-drop-area-active{background:#FF7171}.qq-upload-list{margin:0;padding:0;list-style:none}.qq-upload-list li{margin:0;padding:9px;line-height:15px;font-size:16px;background-color:#FFF0BD}.qq-upload-file,.qq-upload-spinner,.qq-upload-size,.qq-upload-cancel,.qq-upload-retry,.qq-upload-failed-text,.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{margin-right:12px;display:inline}.qq-upload-file{}.qq-upload-spinner{display:inline-block;background:url(loading.gif);width:15px;height:15px;vertical-align:text-bottom}.qq-drop-processing{display:block}.qq-drop-processing-spinner{display:inline-block;background:url(processing.gif);width:24px;height:24px;vertical-align:text-bottom}.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{display:inline}.qq-upload-retry,.qq-upload-delete,.qq-upload-cancel,.qq-upload-pause,.qq-upload-continue{color:#000}.qq-upload-size,.qq-upload-cancel,.qq-upload-retry,.qq-upload-delete,.qq-upload-pause,.qq-upload-continue{font-size:12px;font-weight:400}.qq-upload-failed-text{display:none;font-style:italic;font-weight:700}.qq-upload-failed-icon{display:none;width:15px;height:15px;vertical-align:text-bottom}.qq-upload-fail .qq-upload-failed-text{display:inline}.qq-upload-retrying .qq-upload-failed-text{display:inline;color:#D60000}.qq-upload-list li.qq-upload-success{background-color:#5DA30C;color:#FFF}.qq-upload-list li.qq-upload-fail{background-color:#D60000;color:#FFF}.qq-progress-bar{display:block;background:-moz-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,rgba(30,87,153,1)),color-stop(50%,rgba(41,137,216,1)),color-stop(51%,rgba(32,124,202,1)),color-stop(100%,rgba(125,185,232,1)));background:-webkit-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:-o-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:-ms-linear-gradient(top,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);background:linear-gradient(to bottom,rgba(30,87,153,1) 0,rgba(41,137,216,1) 50%,rgba(32,124,202,1) 51%,rgba(125,185,232,1) 100%);width:0;height:15px;border-radius:6px;margin-bottom:3px}.qq-total-progress-bar{height:25px;border-radius:9px}.qq-total-progress-bar-container{margin:9px}INPUT.qq-edit-filename{position:absolute;opacity:0;filter:alpha(opacity=0);z-index:-1;-ms-filter:"alpha(Opacity=0)"}.qq-upload-file.qq-editable{cursor:pointer}.qq-edit-filename-icon.qq-editable{display:inline-block;cursor:pointer}INPUT.qq-edit-filename.qq-editing{position:static;margin-top:-5px;margin-right:10px;margin-bottom:-5px;opacity:1;filter:alpha(opacity=100);-ms-filter:"alpha(Opacity=100)"}.qq-edit-filename-icon{display:none;background:url(edit.gif);width:15px;height:15px;vertical-align:text-bottom;margin-right:5px}.qq-hide{display:none}.qq-uploader DIALOG{display:none}.qq-uploader DIALOG[open]{display:block}.qq-uploader DIALOG{display:none}.qq-uploader DIALOG[open]{display:block}.qq-uploader DIALOG .qq-dialog-buttons{text-align:center;padding-top:10px}.qq-uploader DIALOG .qq-dialog-buttons BUTTON{margin-left:5px;margin-right:5px}.qq-uploader DIALOG .qq-dialog-message-selector{padding-bottom:10px}.qq-uploader DIALOG::backdrop{background-color:rgba(0,0,0,.7)} +/*! 2016-03-16 */ diff --git a/scripts/fineuploader/iframe.xss.response.js b/scripts/fineuploader/iframe.xss.response.js new file mode 100644 index 000000000..e2cc119f1 --- /dev/null +++ b/scripts/fineuploader/iframe.xss.response.js @@ -0,0 +1,7 @@ +(function() { + "use strict"; + var match = /(\{.*\})/.exec(document.body.innerHTML); + if (match) { + parent.postMessage(match[1], "*"); + } +}()); diff --git a/scripts/fineuploader/jquery.fine-uploader.js b/scripts/fineuploader/jquery.fine-uploader.js new file mode 100644 index 000000000..955b42794 --- /dev/null +++ b/scripts/fineuploader/jquery.fine-uploader.js @@ -0,0 +1,11592 @@ +/*! +* Fine Uploader +* +* Copyright 2015, Widen Enterprises, Inc. info@fineuploader.com +* +* Version: 5.5.2 +* +* Homepage: http://fineuploader.com +* +* Repository: git://github.com/FineUploader/fine-uploader.git +* +* Licensed only under the Widen Commercial License (http://fineuploader.com/licensing). +*/ + + +/*globals window, navigator, document, FormData, File, HTMLInputElement, XMLHttpRequest, Blob, Storage, ActiveXObject */ +/* jshint -W079 */ +var qq = function(element) { + "use strict"; + + return { + hide: function() { + element.style.display = "none"; + return this; + }, + + /** Returns the function which detaches attached event */ + attach: function(type, fn) { + if (element.addEventListener) { + element.addEventListener(type, fn, false); + } else if (element.attachEvent) { + element.attachEvent("on" + type, fn); + } + return function() { + qq(element).detach(type, fn); + }; + }, + + detach: function(type, fn) { + if (element.removeEventListener) { + element.removeEventListener(type, fn, false); + } else if (element.attachEvent) { + element.detachEvent("on" + type, fn); + } + return this; + }, + + contains: function(descendant) { + // The [W3C spec](http://www.w3.org/TR/domcore/#dom-node-contains) + // says a `null` (or ostensibly `undefined`) parameter + // passed into `Node.contains` should result in a false return value. + // IE7 throws an exception if the parameter is `undefined` though. + if (!descendant) { + return false; + } + + // compareposition returns false in this case + if (element === descendant) { + return true; + } + + if (element.contains) { + return element.contains(descendant); + } else { + /*jslint bitwise: true*/ + return !!(descendant.compareDocumentPosition(element) & 8); + } + }, + + /** + * Insert this element before elementB. + */ + insertBefore: function(elementB) { + elementB.parentNode.insertBefore(element, elementB); + return this; + }, + + remove: function() { + element.parentNode.removeChild(element); + return this; + }, + + /** + * Sets styles for an element. + * Fixes opacity in IE6-8. + */ + css: function(styles) { + /*jshint eqnull: true*/ + if (element.style == null) { + throw new qq.Error("Can't apply style to node as it is not on the HTMLElement prototype chain!"); + } + + /*jshint -W116*/ + if (styles.opacity != null) { + if (typeof element.style.opacity !== "string" && typeof (element.filters) !== "undefined") { + styles.filter = "alpha(opacity=" + Math.round(100 * styles.opacity) + ")"; + } + } + qq.extend(element.style, styles); + + return this; + }, + + hasClass: function(name, considerParent) { + var re = new RegExp("(^| )" + name + "( |$)"); + return re.test(element.className) || !!(considerParent && re.test(element.parentNode.className)); + }, + + addClass: function(name) { + if (!qq(element).hasClass(name)) { + element.className += " " + name; + } + return this; + }, + + removeClass: function(name) { + var re = new RegExp("(^| )" + name + "( |$)"); + element.className = element.className.replace(re, " ").replace(/^\s+|\s+$/g, ""); + return this; + }, + + getByClass: function(className, first) { + var candidates, + result = []; + + if (first && element.querySelector) { + return element.querySelector("." + className); + } + else if (element.querySelectorAll) { + return element.querySelectorAll("." + className); + } + + candidates = element.getElementsByTagName("*"); + + qq.each(candidates, function(idx, val) { + if (qq(val).hasClass(className)) { + result.push(val); + } + }); + return first ? result[0] : result; + }, + + getFirstByClass: function(className) { + return qq(element).getByClass(className, true); + }, + + children: function() { + var children = [], + child = element.firstChild; + + while (child) { + if (child.nodeType === 1) { + children.push(child); + } + child = child.nextSibling; + } + + return children; + }, + + setText: function(text) { + element.innerText = text; + element.textContent = text; + return this; + }, + + clearText: function() { + return qq(element).setText(""); + }, + + // Returns true if the attribute exists on the element + // AND the value of the attribute is NOT "false" (case-insensitive) + hasAttribute: function(attrName) { + var attrVal; + + if (element.hasAttribute) { + + if (!element.hasAttribute(attrName)) { + return false; + } + + /*jshint -W116*/ + return (/^false$/i).exec(element.getAttribute(attrName)) == null; + } + else { + attrVal = element[attrName]; + + if (attrVal === undefined) { + return false; + } + + /*jshint -W116*/ + return (/^false$/i).exec(attrVal) == null; + } + } + }; +}; + +(function() { + "use strict"; + + qq.canvasToBlob = function(canvas, mime, quality) { + return qq.dataUriToBlob(canvas.toDataURL(mime, quality)); + }; + + qq.dataUriToBlob = function(dataUri) { + var arrayBuffer, byteString, + createBlob = function(data, mime) { + var BlobBuilder = window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder, + blobBuilder = BlobBuilder && new BlobBuilder(); + + if (blobBuilder) { + blobBuilder.append(data); + return blobBuilder.getBlob(mime); + } + else { + return new Blob([data], {type: mime}); + } + }, + intArray, mimeString; + + // convert base64 to raw binary data held in a string + if (dataUri.split(",")[0].indexOf("base64") >= 0) { + byteString = atob(dataUri.split(",")[1]); + } + else { + byteString = decodeURI(dataUri.split(",")[1]); + } + + // extract the MIME + mimeString = dataUri.split(",")[0] + .split(":")[1] + .split(";")[0]; + + // write the bytes of the binary string to an ArrayBuffer + arrayBuffer = new ArrayBuffer(byteString.length); + intArray = new Uint8Array(arrayBuffer); + qq.each(byteString, function(idx, character) { + intArray[idx] = character.charCodeAt(0); + }); + + return createBlob(arrayBuffer, mimeString); + }; + + qq.log = function(message, level) { + if (window.console) { + if (!level || level === "info") { + window.console.log(message); + } + else + { + if (window.console[level]) { + window.console[level](message); + } + else { + window.console.log("<" + level + "> " + message); + } + } + } + }; + + qq.isObject = function(variable) { + return variable && !variable.nodeType && Object.prototype.toString.call(variable) === "[object Object]"; + }; + + qq.isFunction = function(variable) { + return typeof (variable) === "function"; + }; + + /** + * Check the type of a value. Is it an "array"? + * + * @param value value to test. + * @returns true if the value is an array or associated with an `ArrayBuffer` + */ + qq.isArray = function(value) { + return Object.prototype.toString.call(value) === "[object Array]" || + (value && window.ArrayBuffer && value.buffer && value.buffer.constructor === ArrayBuffer); + }; + + // Looks for an object on a `DataTransfer` object that is associated with drop events when utilizing the Filesystem API. + qq.isItemList = function(maybeItemList) { + return Object.prototype.toString.call(maybeItemList) === "[object DataTransferItemList]"; + }; + + // Looks for an object on a `NodeList` or an `HTMLCollection`|`HTMLFormElement`|`HTMLSelectElement` + // object that is associated with collections of Nodes. + qq.isNodeList = function(maybeNodeList) { + return Object.prototype.toString.call(maybeNodeList) === "[object NodeList]" || + // If `HTMLCollection` is the actual type of the object, we must determine this + // by checking for expected properties/methods on the object + (maybeNodeList.item && maybeNodeList.namedItem); + }; + + qq.isString = function(maybeString) { + return Object.prototype.toString.call(maybeString) === "[object String]"; + }; + + qq.trimStr = function(string) { + if (String.prototype.trim) { + return string.trim(); + } + + return string.replace(/^\s+|\s+$/g, ""); + }; + + /** + * @param str String to format. + * @returns {string} A string, swapping argument values with the associated occurrence of {} in the passed string. + */ + qq.format = function(str) { + + var args = Array.prototype.slice.call(arguments, 1), + newStr = str, + nextIdxToReplace = newStr.indexOf("{}"); + + qq.each(args, function(idx, val) { + var strBefore = newStr.substring(0, nextIdxToReplace), + strAfter = newStr.substring(nextIdxToReplace + 2); + + newStr = strBefore + val + strAfter; + nextIdxToReplace = newStr.indexOf("{}", nextIdxToReplace + val.length); + + // End the loop if we have run out of tokens (when the arguments exceed the # of tokens) + if (nextIdxToReplace < 0) { + return false; + } + }); + + return newStr; + }; + + qq.isFile = function(maybeFile) { + return window.File && Object.prototype.toString.call(maybeFile) === "[object File]"; + }; + + qq.isFileList = function(maybeFileList) { + return window.FileList && Object.prototype.toString.call(maybeFileList) === "[object FileList]"; + }; + + qq.isFileOrInput = function(maybeFileOrInput) { + return qq.isFile(maybeFileOrInput) || qq.isInput(maybeFileOrInput); + }; + + qq.isInput = function(maybeInput, notFile) { + var evaluateType = function(type) { + var normalizedType = type.toLowerCase(); + + if (notFile) { + return normalizedType !== "file"; + } + + return normalizedType === "file"; + }; + + if (window.HTMLInputElement) { + if (Object.prototype.toString.call(maybeInput) === "[object HTMLInputElement]") { + if (maybeInput.type && evaluateType(maybeInput.type)) { + return true; + } + } + } + if (maybeInput.tagName) { + if (maybeInput.tagName.toLowerCase() === "input") { + if (maybeInput.type && evaluateType(maybeInput.type)) { + return true; + } + } + } + + return false; + }; + + qq.isBlob = function(maybeBlob) { + if (window.Blob && Object.prototype.toString.call(maybeBlob) === "[object Blob]") { + return true; + } + }; + + qq.isXhrUploadSupported = function() { + var input = document.createElement("input"); + input.type = "file"; + + return ( + input.multiple !== undefined && + typeof File !== "undefined" && + typeof FormData !== "undefined" && + typeof (qq.createXhrInstance()).upload !== "undefined"); + }; + + // Fall back to ActiveX is native XHR is disabled (possible in any version of IE). + qq.createXhrInstance = function() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } + + try { + return new ActiveXObject("MSXML2.XMLHTTP.3.0"); + } + catch (error) { + qq.log("Neither XHR or ActiveX are supported!", "error"); + return null; + } + }; + + qq.isFolderDropSupported = function(dataTransfer) { + return dataTransfer.items && + dataTransfer.items.length > 0 && + dataTransfer.items[0].webkitGetAsEntry; + }; + + qq.isFileChunkingSupported = function() { + return !qq.androidStock() && //Android's stock browser cannot upload Blobs correctly + qq.isXhrUploadSupported() && + (File.prototype.slice !== undefined || File.prototype.webkitSlice !== undefined || File.prototype.mozSlice !== undefined); + }; + + qq.sliceBlob = function(fileOrBlob, start, end) { + var slicer = fileOrBlob.slice || fileOrBlob.mozSlice || fileOrBlob.webkitSlice; + + return slicer.call(fileOrBlob, start, end); + }; + + qq.arrayBufferToHex = function(buffer) { + var bytesAsHex = "", + bytes = new Uint8Array(buffer); + + qq.each(bytes, function(idx, byt) { + var byteAsHexStr = byt.toString(16); + + if (byteAsHexStr.length < 2) { + byteAsHexStr = "0" + byteAsHexStr; + } + + bytesAsHex += byteAsHexStr; + }); + + return bytesAsHex; + }; + + qq.readBlobToHex = function(blob, startOffset, length) { + var initialBlob = qq.sliceBlob(blob, startOffset, startOffset + length), + fileReader = new FileReader(), + promise = new qq.Promise(); + + fileReader.onload = function() { + promise.success(qq.arrayBufferToHex(fileReader.result)); + }; + + fileReader.onerror = promise.failure; + + fileReader.readAsArrayBuffer(initialBlob); + + return promise; + }; + + qq.extend = function(first, second, extendNested) { + qq.each(second, function(prop, val) { + if (extendNested && qq.isObject(val)) { + if (first[prop] === undefined) { + first[prop] = {}; + } + qq.extend(first[prop], val, true); + } + else { + first[prop] = val; + } + }); + + return first; + }; + + /** + * Allow properties in one object to override properties in another, + * keeping track of the original values from the target object. + * + * Note that the pre-overriden properties to be overriden by the source will be passed into the `sourceFn` when it is invoked. + * + * @param target Update properties in this object from some source + * @param sourceFn A function that, when invoked, will return properties that will replace properties with the same name in the target. + * @returns {object} The target object + */ + qq.override = function(target, sourceFn) { + var super_ = {}, + source = sourceFn(super_); + + qq.each(source, function(srcPropName, srcPropVal) { + if (target[srcPropName] !== undefined) { + super_[srcPropName] = target[srcPropName]; + } + + target[srcPropName] = srcPropVal; + }); + + return target; + }; + + /** + * Searches for a given element (elt) in the array, returns -1 if it is not present. + */ + qq.indexOf = function(arr, elt, from) { + if (arr.indexOf) { + return arr.indexOf(elt, from); + } + + from = from || 0; + var len = arr.length; + + if (from < 0) { + from += len; + } + + for (; from < len; from += 1) { + if (arr.hasOwnProperty(from) && arr[from] === elt) { + return from; + } + } + return -1; + }; + + //this is a version 4 UUID + qq.getUniqueId = function() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + /*jslint eqeq: true, bitwise: true*/ + var r = Math.random() * 16 | 0, v = c == "x" ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + }; + + // + // Browsers and platforms detection + qq.ie = function() { + return navigator.userAgent.indexOf("MSIE") !== -1 || + navigator.userAgent.indexOf("Trident") !== -1; + }; + + qq.ie7 = function() { + return navigator.userAgent.indexOf("MSIE 7") !== -1; + }; + + qq.ie8 = function() { + return navigator.userAgent.indexOf("MSIE 8") !== -1; + }; + + qq.ie10 = function() { + return navigator.userAgent.indexOf("MSIE 10") !== -1; + }; + + qq.ie11 = function() { + return qq.ie() && navigator.userAgent.indexOf("rv:11") !== -1; + }; + + qq.safari = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Apple") !== -1; + }; + + qq.chrome = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Google") !== -1; + }; + + qq.opera = function() { + return navigator.vendor !== undefined && navigator.vendor.indexOf("Opera") !== -1; + }; + + qq.firefox = function() { + return (!qq.ie11() && navigator.userAgent.indexOf("Mozilla") !== -1 && navigator.vendor !== undefined && navigator.vendor === ""); + }; + + qq.windows = function() { + return navigator.platform === "Win32"; + }; + + qq.android = function() { + return navigator.userAgent.toLowerCase().indexOf("android") !== -1; + }; + + // We need to identify the Android stock browser via the UA string to work around various bugs in this browser, + // such as the one that prevents a `Blob` from being uploaded. + qq.androidStock = function() { + return qq.android() && navigator.userAgent.toLowerCase().indexOf("chrome") < 0; + }; + + qq.ios6 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 6_") !== -1; + }; + + qq.ios7 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 7_") !== -1; + }; + + qq.ios8 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_") !== -1; + }; + + // iOS 8.0.0 + qq.ios800 = function() { + return qq.ios() && navigator.userAgent.indexOf(" OS 8_0 ") !== -1; + }; + + qq.ios = function() { + /*jshint -W014 */ + return navigator.userAgent.indexOf("iPad") !== -1 + || navigator.userAgent.indexOf("iPod") !== -1 + || navigator.userAgent.indexOf("iPhone") !== -1; + }; + + qq.iosChrome = function() { + return qq.ios() && navigator.userAgent.indexOf("CriOS") !== -1; + }; + + qq.iosSafari = function() { + return qq.ios() && !qq.iosChrome() && navigator.userAgent.indexOf("Safari") !== -1; + }; + + qq.iosSafariWebView = function() { + return qq.ios() && !qq.iosChrome() && !qq.iosSafari(); + }; + + // + // Events + + qq.preventDefault = function(e) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + }; + + /** + * Creates and returns element from html string + * Uses innerHTML to create an element + */ + qq.toElement = (function() { + var div = document.createElement("div"); + return function(html) { + div.innerHTML = html; + var element = div.firstChild; + div.removeChild(element); + return element; + }; + }()); + + //key and value are passed to callback for each entry in the iterable item + qq.each = function(iterableItem, callback) { + var keyOrIndex, retVal; + + if (iterableItem) { + // Iterate through [`Storage`](http://www.w3.org/TR/webstorage/#the-storage-interface) items + if (window.Storage && iterableItem.constructor === window.Storage) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(iterableItem.key(keyOrIndex), iterableItem.getItem(iterableItem.key(keyOrIndex))); + if (retVal === false) { + break; + } + } + } + // `DataTransferItemList` & `NodeList` objects are array-like and should be treated as arrays + // when iterating over items inside the object. + else if (qq.isArray(iterableItem) || qq.isItemList(iterableItem) || qq.isNodeList(iterableItem)) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); + if (retVal === false) { + break; + } + } + } + else if (qq.isString(iterableItem)) { + for (keyOrIndex = 0; keyOrIndex < iterableItem.length; keyOrIndex++) { + retVal = callback(keyOrIndex, iterableItem.charAt(keyOrIndex)); + if (retVal === false) { + break; + } + } + } + else { + for (keyOrIndex in iterableItem) { + if (Object.prototype.hasOwnProperty.call(iterableItem, keyOrIndex)) { + retVal = callback(keyOrIndex, iterableItem[keyOrIndex]); + if (retVal === false) { + break; + } + } + } + } + } + }; + + //include any args that should be passed to the new function after the context arg + qq.bind = function(oldFunc, context) { + if (qq.isFunction(oldFunc)) { + var args = Array.prototype.slice.call(arguments, 2); + + return function() { + var newArgs = qq.extend([], args); + if (arguments.length) { + newArgs = newArgs.concat(Array.prototype.slice.call(arguments)); + } + return oldFunc.apply(context, newArgs); + }; + } + + throw new Error("first parameter must be a function!"); + }; + + /** + * obj2url() takes a json-object as argument and generates + * a querystring. pretty much like jQuery.param() + * + * how to use: + * + * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` + * + * will result in: + * + * `http://any.url/upload?otherParam=value&a=b&c=d` + * + * @param Object JSON-Object + * @param String current querystring-part + * @return String encoded querystring + */ + qq.obj2url = function(obj, temp, prefixDone) { + /*jshint laxbreak: true*/ + var uristrings = [], + prefix = "&", + add = function(nextObj, i) { + var nextTemp = temp + ? (/\[\]$/.test(temp)) // prevent double-encoding + ? temp + : temp + "[" + i + "]" + : i; + if ((nextTemp !== "undefined") && (i !== "undefined")) { + uristrings.push( + (typeof nextObj === "object") + ? qq.obj2url(nextObj, nextTemp, true) + : (Object.prototype.toString.call(nextObj) === "[object Function]") + ? encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj()) + : encodeURIComponent(nextTemp) + "=" + encodeURIComponent(nextObj) + ); + } + }; + + if (!prefixDone && temp) { + prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? "" : "&" : "?"; + uristrings.push(temp); + uristrings.push(qq.obj2url(obj)); + } else if ((Object.prototype.toString.call(obj) === "[object Array]") && (typeof obj !== "undefined")) { + qq.each(obj, function(idx, val) { + add(val, idx); + }); + } else if ((typeof obj !== "undefined") && (obj !== null) && (typeof obj === "object")) { + qq.each(obj, function(prop, val) { + add(val, prop); + }); + } else { + uristrings.push(encodeURIComponent(temp) + "=" + encodeURIComponent(obj)); + } + + if (temp) { + return uristrings.join(prefix); + } else { + return uristrings.join(prefix) + .replace(/^&/, "") + .replace(/%20/g, "+"); + } + }; + + qq.obj2FormData = function(obj, formData, arrayKeyName) { + if (!formData) { + formData = new FormData(); + } + + qq.each(obj, function(key, val) { + key = arrayKeyName ? arrayKeyName + "[" + key + "]" : key; + + if (qq.isObject(val)) { + qq.obj2FormData(val, formData, key); + } + else if (qq.isFunction(val)) { + formData.append(key, val()); + } + else { + formData.append(key, val); + } + }); + + return formData; + }; + + qq.obj2Inputs = function(obj, form) { + var input; + + if (!form) { + form = document.createElement("form"); + } + + qq.obj2FormData(obj, { + append: function(key, val) { + input = document.createElement("input"); + input.setAttribute("name", key); + input.setAttribute("value", val); + form.appendChild(input); + } + }); + + return form; + }; + + /** + * Not recommended for use outside of Fine Uploader since this falls back to an unchecked eval if JSON.parse is not + * implemented. For a more secure JSON.parse polyfill, use Douglas Crockford's json2.js. + */ + qq.parseJson = function(json) { + /*jshint evil: true*/ + if (window.JSON && qq.isFunction(JSON.parse)) { + return JSON.parse(json); + } else { + return eval("(" + json + ")"); + } + }; + + /** + * Retrieve the extension of a file, if it exists. + * + * @param filename + * @returns {string || undefined} + */ + qq.getExtension = function(filename) { + var extIdx = filename.lastIndexOf(".") + 1; + + if (extIdx > 0) { + return filename.substr(extIdx, filename.length - extIdx); + } + }; + + qq.getFilename = function(blobOrFileInput) { + /*jslint regexp: true*/ + + if (qq.isInput(blobOrFileInput)) { + // get input value and remove path to normalize + return blobOrFileInput.value.replace(/.*(\/|\\)/, ""); + } + else if (qq.isFile(blobOrFileInput)) { + if (blobOrFileInput.fileName !== null && blobOrFileInput.fileName !== undefined) { + return blobOrFileInput.fileName; + } + } + + return blobOrFileInput.name; + }; + + /** + * A generic module which supports object disposing in dispose() method. + * */ + qq.DisposeSupport = function() { + var disposers = []; + + return { + /** Run all registered disposers */ + dispose: function() { + var disposer; + do { + disposer = disposers.shift(); + if (disposer) { + disposer(); + } + } + while (disposer); + }, + + /** Attach event handler and register de-attacher as a disposer */ + attach: function() { + var args = arguments; + /*jslint undef:true*/ + this.addDisposer(qq(args[0]).attach.apply(this, Array.prototype.slice.call(arguments, 1))); + }, + + /** Add disposer to the collection */ + addDisposer: function(disposeFunction) { + disposers.push(disposeFunction); + } + }; + }; +}()); + +/* globals qq */ +/** + * Fine Uploader top-level Error container. Inherits from `Error`. + */ +(function() { + "use strict"; + + qq.Error = function(message) { + this.message = "[Fine Uploader " + qq.version + "] " + message; + }; + + qq.Error.prototype = new Error(); +}()); + +/*global qq */ +qq.version = "5.5.2"; + +/* globals qq */ +qq.supportedFeatures = (function() { + "use strict"; + + var supportsUploading, + supportsUploadingBlobs, + supportsFileDrop, + supportsAjaxFileUploading, + supportsFolderDrop, + supportsChunking, + supportsResume, + supportsUploadViaPaste, + supportsUploadCors, + supportsDeleteFileXdr, + supportsDeleteFileCorsXhr, + supportsDeleteFileCors, + supportsFolderSelection, + supportsImagePreviews, + supportsUploadProgress; + + function testSupportsFileInputElement() { + var supported = true, + tempInput; + + try { + tempInput = document.createElement("input"); + tempInput.type = "file"; + qq(tempInput).hide(); + + if (tempInput.disabled) { + supported = false; + } + } + catch (ex) { + supported = false; + } + + return supported; + } + + //only way to test for Filesystem API support since webkit does not expose the DataTransfer interface + function isChrome21OrHigher() { + return (qq.chrome() || qq.opera()) && + navigator.userAgent.match(/Chrome\/[2][1-9]|Chrome\/[3-9][0-9]/) !== undefined; + } + + //only way to test for complete Clipboard API support at this time + function isChrome14OrHigher() { + return (qq.chrome() || qq.opera()) && + navigator.userAgent.match(/Chrome\/[1][4-9]|Chrome\/[2-9][0-9]/) !== undefined; + } + + //Ensure we can send cross-origin `XMLHttpRequest`s + function isCrossOriginXhrSupported() { + if (window.XMLHttpRequest) { + var xhr = qq.createXhrInstance(); + + //Commonly accepted test for XHR CORS support. + return xhr.withCredentials !== undefined; + } + + return false; + } + + //Test for (terrible) cross-origin ajax transport fallback for IE9 and IE8 + function isXdrSupported() { + return window.XDomainRequest !== undefined; + } + + // CORS Ajax requests are supported if it is either possible to send credentialed `XMLHttpRequest`s, + // or if `XDomainRequest` is an available alternative. + function isCrossOriginAjaxSupported() { + if (isCrossOriginXhrSupported()) { + return true; + } + + return isXdrSupported(); + } + + function isFolderSelectionSupported() { + // We know that folder selection is only supported in Chrome via this proprietary attribute for now + return document.createElement("input").webkitdirectory !== undefined; + } + + function isLocalStorageSupported() { + try { + return !!window.localStorage && + // unpatched versions of IE10/11 have buggy impls of localStorage where setItem is a string + qq.isFunction(window.localStorage.setItem); + } + catch (error) { + // probably caught a security exception, so no localStorage for you + return false; + } + } + + function isDragAndDropSupported() { + var span = document.createElement("span"); + + return ("draggable" in span || ("ondragstart" in span && "ondrop" in span)) && + !qq.android() && !qq.ios(); + } + + supportsUploading = testSupportsFileInputElement(); + + supportsAjaxFileUploading = supportsUploading && qq.isXhrUploadSupported(); + + supportsUploadingBlobs = supportsAjaxFileUploading && !qq.androidStock(); + + supportsFileDrop = supportsAjaxFileUploading && isDragAndDropSupported(); + + supportsFolderDrop = supportsFileDrop && isChrome21OrHigher(); + + supportsChunking = supportsAjaxFileUploading && qq.isFileChunkingSupported(); + + supportsResume = supportsAjaxFileUploading && supportsChunking && isLocalStorageSupported(); + + supportsUploadViaPaste = supportsAjaxFileUploading && isChrome14OrHigher(); + + supportsUploadCors = supportsUploading && (window.postMessage !== undefined || supportsAjaxFileUploading); + + supportsDeleteFileCorsXhr = isCrossOriginXhrSupported(); + + supportsDeleteFileXdr = isXdrSupported(); + + supportsDeleteFileCors = isCrossOriginAjaxSupported(); + + supportsFolderSelection = isFolderSelectionSupported(); + + supportsImagePreviews = supportsAjaxFileUploading && window.FileReader !== undefined; + + supportsUploadProgress = (function() { + if (supportsAjaxFileUploading) { + return !qq.androidStock() && !qq.iosChrome(); + } + return false; + }()); + + return { + ajaxUploading: supportsAjaxFileUploading, + blobUploading: supportsUploadingBlobs, + canDetermineSize: supportsAjaxFileUploading, + chunking: supportsChunking, + deleteFileCors: supportsDeleteFileCors, + deleteFileCorsXdr: supportsDeleteFileXdr, //NOTE: will also return true in IE10, where XDR is also supported + deleteFileCorsXhr: supportsDeleteFileCorsXhr, + dialogElement: !!window.HTMLDialogElement, + fileDrop: supportsFileDrop, + folderDrop: supportsFolderDrop, + folderSelection: supportsFolderSelection, + imagePreviews: supportsImagePreviews, + imageValidation: supportsImagePreviews, + itemSizeValidation: supportsAjaxFileUploading, + pause: supportsChunking, + progressBar: supportsUploadProgress, + resume: supportsResume, + scaling: supportsImagePreviews && supportsUploadingBlobs, + tiffPreviews: qq.safari(), // Not the best solution, but simple and probably accurate enough (for now) + unlimitedScaledImageSize: !qq.ios(), // false simply indicates that there is some known limit + uploading: supportsUploading, + uploadCors: supportsUploadCors, + uploadCustomHeaders: supportsAjaxFileUploading, + uploadNonMultipart: supportsAjaxFileUploading, + uploadViaPaste: supportsUploadViaPaste + }; + +}()); + +/*globals qq*/ + +// Is the passed object a promise instance? +qq.isGenericPromise = function(maybePromise) { + "use strict"; + return !!(maybePromise && maybePromise.then && qq.isFunction(maybePromise.then)); +}; + +qq.Promise = function() { + "use strict"; + + var successArgs, failureArgs, + successCallbacks = [], + failureCallbacks = [], + doneCallbacks = [], + state = 0; + + qq.extend(this, { + then: function(onSuccess, onFailure) { + if (state === 0) { + if (onSuccess) { + successCallbacks.push(onSuccess); + } + if (onFailure) { + failureCallbacks.push(onFailure); + } + } + else if (state === -1) { + onFailure && onFailure.apply(null, failureArgs); + } + else if (onSuccess) { + onSuccess.apply(null, successArgs); + } + + return this; + }, + + done: function(callback) { + if (state === 0) { + doneCallbacks.push(callback); + } + else { + callback.apply(null, failureArgs === undefined ? successArgs : failureArgs); + } + + return this; + }, + + success: function() { + state = 1; + successArgs = arguments; + + if (successCallbacks.length) { + qq.each(successCallbacks, function(idx, callback) { + callback.apply(null, successArgs); + }); + } + + if (doneCallbacks.length) { + qq.each(doneCallbacks, function(idx, callback) { + callback.apply(null, successArgs); + }); + } + + return this; + }, + + failure: function() { + state = -1; + failureArgs = arguments; + + if (failureCallbacks.length) { + qq.each(failureCallbacks, function(idx, callback) { + callback.apply(null, failureArgs); + }); + } + + if (doneCallbacks.length) { + qq.each(doneCallbacks, function(idx, callback) { + callback.apply(null, failureArgs); + }); + } + + return this; + } + }); +}; + +/* globals qq */ +/** + * Placeholder for a Blob that will be generated on-demand. + * + * @param referenceBlob Parent of the generated blob + * @param onCreate Function to invoke when the blob must be created. Must be promissory. + * @constructor + */ +qq.BlobProxy = function(referenceBlob, onCreate) { + "use strict"; + + qq.extend(this, { + referenceBlob: referenceBlob, + + create: function() { + return onCreate(referenceBlob); + } + }); +}; + +/*globals qq*/ + +/** + * This module represents an upload or "Select File(s)" button. It's job is to embed an opaque `` + * element as a child of a provided "container" element. This "container" element (`options.element`) is used to provide + * a custom style for the `` element. The ability to change the style of the container element is also + * provided here by adding CSS classes to the container on hover/focus. + * + * TODO Eliminate the mouseover and mouseout event handlers since the :hover CSS pseudo-class should now be + * available on all supported browsers. + * + * @param o Options to override the default values + */ +qq.UploadButton = function(o) { + "use strict"; + + var self = this, + + disposeSupport = new qq.DisposeSupport(), + + options = { + // "Container" element + element: null, + + // If true adds `multiple` attribute to `` + multiple: false, + + // Corresponds to the `accept` attribute on the associated `` + acceptFiles: null, + + // A true value allows folders to be selected, if supported by the UA + folders: false, + + // `name` attribute of `` + name: "qqfile", + + // Called when the browser invokes the onchange handler on the `` + onChange: function(input) {}, + + ios8BrowserCrashWorkaround: false, + + // **This option will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers + hoverClass: "qq-upload-button-hover", + + focusClass: "qq-upload-button-focus" + }, + input, buttonId; + + // Overrides any of the default option values with any option values passed in during construction. + qq.extend(options, o); + + buttonId = qq.getUniqueId(); + + // Embed an opaque `` element as a child of `options.element`. + function createInput() { + var input = document.createElement("input"); + + input.setAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME, buttonId); + input.setAttribute("title", "file input"); + + self.setMultiple(options.multiple, input); + + if (options.folders && qq.supportedFeatures.folderSelection) { + // selecting directories is only possible in Chrome now, via a vendor-specific prefixed attribute + input.setAttribute("webkitdirectory", ""); + } + + if (options.acceptFiles) { + input.setAttribute("accept", options.acceptFiles); + } + + input.setAttribute("type", "file"); + input.setAttribute("name", options.name); + + qq(input).css({ + position: "absolute", + // in Opera only 'browse' button + // is clickable and it is located at + // the right side of the input + right: 0, + top: 0, + fontFamily: "Arial", + // It's especially important to make this an arbitrarily large value + // to ensure the rendered input button in IE takes up the entire + // space of the container element. Otherwise, the left side of the + // button will require a double-click to invoke the file chooser. + // In other browsers, this might cause other issues, so a large font-size + // is only used in IE. There is a bug in IE8 where the opacity style is ignored + // in some cases when the font-size is large. So, this workaround is not applied + // to IE8. + fontSize: qq.ie() && !qq.ie8() ? "3500px" : "118px", + margin: 0, + padding: 0, + cursor: "pointer", + opacity: 0 + }); + + // Setting the file input's height to 100% in IE7 causes + // most of the visible button to be unclickable. + !qq.ie7() && qq(input).css({height: "100%"}); + + options.element.appendChild(input); + + disposeSupport.attach(input, "change", function() { + options.onChange(input); + }); + + // **These event handlers will be removed** in the future as the :hover CSS pseudo-class is available on all supported browsers + disposeSupport.attach(input, "mouseover", function() { + qq(options.element).addClass(options.hoverClass); + }); + disposeSupport.attach(input, "mouseout", function() { + qq(options.element).removeClass(options.hoverClass); + }); + + disposeSupport.attach(input, "focus", function() { + qq(options.element).addClass(options.focusClass); + }); + disposeSupport.attach(input, "blur", function() { + qq(options.element).removeClass(options.focusClass); + }); + + return input; + } + + // Make button suitable container for input + qq(options.element).css({ + position: "relative", + overflow: "hidden", + // Make sure browse button is in the right side in Internet Explorer + direction: "ltr" + }); + + // Exposed API + qq.extend(this, { + getInput: function() { + return input; + }, + + getButtonId: function() { + return buttonId; + }, + + setMultiple: function(isMultiple, optInput) { + var input = optInput || this.getInput(); + + // Temporary workaround for bug in in iOS8 UIWebView that causes the browser to crash + // before the file chooser appears if the file input doesn't contain a multiple attribute. + // See #1283. + if (options.ios8BrowserCrashWorkaround && qq.ios8() && (qq.iosChrome() || qq.iosSafariWebView())) { + input.setAttribute("multiple", ""); + } + + else { + if (isMultiple) { + input.setAttribute("multiple", ""); + } + else { + input.removeAttribute("multiple"); + } + } + }, + + setAcceptFiles: function(acceptFiles) { + if (acceptFiles !== options.acceptFiles) { + input.setAttribute("accept", acceptFiles); + } + }, + + reset: function() { + if (input.parentNode) { + qq(input).remove(); + } + + qq(options.element).removeClass(options.focusClass); + input = null; + input = createInput(); + } + }); + + input = createInput(); +}; + +qq.UploadButton.BUTTON_ID_ATTR_NAME = "qq-button-id"; + +/*globals qq */ +qq.UploadData = function(uploaderProxy) { + "use strict"; + + var data = [], + byUuid = {}, + byStatus = {}, + byProxyGroupId = {}, + byBatchId = {}; + + function getDataByIds(idOrIds) { + if (qq.isArray(idOrIds)) { + var entries = []; + + qq.each(idOrIds, function(idx, id) { + entries.push(data[id]); + }); + + return entries; + } + + return data[idOrIds]; + } + + function getDataByUuids(uuids) { + if (qq.isArray(uuids)) { + var entries = []; + + qq.each(uuids, function(idx, uuid) { + entries.push(data[byUuid[uuid]]); + }); + + return entries; + } + + return data[byUuid[uuids]]; + } + + function getDataByStatus(status) { + var statusResults = [], + statuses = [].concat(status); + + qq.each(statuses, function(index, statusEnum) { + var statusResultIndexes = byStatus[statusEnum]; + + if (statusResultIndexes !== undefined) { + qq.each(statusResultIndexes, function(i, dataIndex) { + statusResults.push(data[dataIndex]); + }); + } + }); + + return statusResults; + } + + qq.extend(this, { + /** + * Adds a new file to the data cache for tracking purposes. + * + * @param spec Data that describes this file. Possible properties are: + * + * - uuid: Initial UUID for this file. + * - name: Initial name of this file. + * - size: Size of this file, omit if this cannot be determined + * - status: Initial `qq.status` for this file. Omit for `qq.status.SUBMITTING`. + * - batchId: ID of the batch this file belongs to + * - proxyGroupId: ID of the proxy group associated with this file + * + * @returns {number} Internal ID for this file. + */ + addFile: function(spec) { + var status = spec.status || qq.status.SUBMITTING, + id = data.push({ + name: spec.name, + originalName: spec.name, + uuid: spec.uuid, + size: spec.size == null ? -1 : spec.size, + status: status + }) - 1; + + if (spec.batchId) { + data[id].batchId = spec.batchId; + + if (byBatchId[spec.batchId] === undefined) { + byBatchId[spec.batchId] = []; + } + byBatchId[spec.batchId].push(id); + } + + if (spec.proxyGroupId) { + data[id].proxyGroupId = spec.proxyGroupId; + + if (byProxyGroupId[spec.proxyGroupId] === undefined) { + byProxyGroupId[spec.proxyGroupId] = []; + } + byProxyGroupId[spec.proxyGroupId].push(id); + } + + data[id].id = id; + byUuid[spec.uuid] = id; + + if (byStatus[status] === undefined) { + byStatus[status] = []; + } + byStatus[status].push(id); + + uploaderProxy.onStatusChange(id, null, status); + + return id; + }, + + retrieve: function(optionalFilter) { + if (qq.isObject(optionalFilter) && data.length) { + if (optionalFilter.id !== undefined) { + return getDataByIds(optionalFilter.id); + } + + else if (optionalFilter.uuid !== undefined) { + return getDataByUuids(optionalFilter.uuid); + } + + else if (optionalFilter.status) { + return getDataByStatus(optionalFilter.status); + } + } + else { + return qq.extend([], data, true); + } + }, + + reset: function() { + data = []; + byUuid = {}; + byStatus = {}; + byBatchId = {}; + }, + + setStatus: function(id, newStatus) { + var oldStatus = data[id].status, + byStatusOldStatusIndex = qq.indexOf(byStatus[oldStatus], id); + + byStatus[oldStatus].splice(byStatusOldStatusIndex, 1); + + data[id].status = newStatus; + + if (byStatus[newStatus] === undefined) { + byStatus[newStatus] = []; + } + byStatus[newStatus].push(id); + + uploaderProxy.onStatusChange(id, oldStatus, newStatus); + }, + + uuidChanged: function(id, newUuid) { + var oldUuid = data[id].uuid; + + data[id].uuid = newUuid; + byUuid[newUuid] = id; + delete byUuid[oldUuid]; + }, + + updateName: function(id, newName) { + data[id].name = newName; + }, + + updateSize: function(id, newSize) { + data[id].size = newSize; + }, + + // Only applicable if this file has a parent that we may want to reference later. + setParentId: function(targetId, parentId) { + data[targetId].parentId = parentId; + }, + + getIdsInProxyGroup: function(id) { + var proxyGroupId = data[id].proxyGroupId; + + if (proxyGroupId) { + return byProxyGroupId[proxyGroupId]; + } + return []; + }, + + getIdsInBatch: function(id) { + var batchId = data[id].batchId; + + return byBatchId[batchId]; + } + }); +}; + +qq.status = { + SUBMITTING: "submitting", + SUBMITTED: "submitted", + REJECTED: "rejected", + QUEUED: "queued", + CANCELED: "canceled", + PAUSED: "paused", + UPLOADING: "uploading", + UPLOAD_RETRYING: "retrying upload", + UPLOAD_SUCCESSFUL: "upload successful", + UPLOAD_FAILED: "upload failed", + DELETE_FAILED: "delete failed", + DELETING: "deleting", + DELETED: "deleted" +}; + +/*globals qq*/ +/** + * Defines the public API for FineUploaderBasic mode. + */ +(function() { + "use strict"; + + qq.basePublicApi = { + // DEPRECATED - TODO REMOVE IN NEXT MAJOR RELEASE (replaced by addFiles) + addBlobs: function(blobDataOrArray, params, endpoint) { + this.addFiles(blobDataOrArray, params, endpoint); + }, + + addFiles: function(data, params, endpoint) { + this._maybeHandleIos8SafariWorkaround(); + + var batchId = this._storedIds.length === 0 ? qq.getUniqueId() : this._currentBatchId, + + processBlob = qq.bind(function(blob) { + this._handleNewFile({ + blob: blob, + name: this._options.blobs.defaultName + }, batchId, verifiedFiles); + }, this), + + processBlobData = qq.bind(function(blobData) { + this._handleNewFile(blobData, batchId, verifiedFiles); + }, this), + + processCanvas = qq.bind(function(canvas) { + var blob = qq.canvasToBlob(canvas); + + this._handleNewFile({ + blob: blob, + name: this._options.blobs.defaultName + ".png" + }, batchId, verifiedFiles); + }, this), + + processCanvasData = qq.bind(function(canvasData) { + var normalizedQuality = canvasData.quality && canvasData.quality / 100, + blob = qq.canvasToBlob(canvasData.canvas, canvasData.type, normalizedQuality); + + this._handleNewFile({ + blob: blob, + name: canvasData.name + }, batchId, verifiedFiles); + }, this), + + processFileOrInput = qq.bind(function(fileOrInput) { + if (qq.isInput(fileOrInput) && qq.supportedFeatures.ajaxUploading) { + var files = Array.prototype.slice.call(fileOrInput.files), + self = this; + + qq.each(files, function(idx, file) { + self._handleNewFile(file, batchId, verifiedFiles); + }); + } + else { + this._handleNewFile(fileOrInput, batchId, verifiedFiles); + } + }, this), + + normalizeData = function() { + if (qq.isFileList(data)) { + data = Array.prototype.slice.call(data); + } + data = [].concat(data); + }, + + self = this, + verifiedFiles = []; + + this._currentBatchId = batchId; + + if (data) { + normalizeData(); + + qq.each(data, function(idx, fileContainer) { + if (qq.isFileOrInput(fileContainer)) { + processFileOrInput(fileContainer); + } + else if (qq.isBlob(fileContainer)) { + processBlob(fileContainer); + } + else if (qq.isObject(fileContainer)) { + if (fileContainer.blob && fileContainer.name) { + processBlobData(fileContainer); + } + else if (fileContainer.canvas && fileContainer.name) { + processCanvasData(fileContainer); + } + } + else if (fileContainer.tagName && fileContainer.tagName.toLowerCase() === "canvas") { + processCanvas(fileContainer); + } + else { + self.log(fileContainer + " is not a valid file container! Ignoring!", "warn"); + } + }); + + this.log("Received " + verifiedFiles.length + " files."); + this._prepareItemsForUpload(verifiedFiles, params, endpoint); + } + }, + + cancel: function(id) { + this._handler.cancel(id); + }, + + cancelAll: function() { + var storedIdsCopy = [], + self = this; + + qq.extend(storedIdsCopy, this._storedIds); + qq.each(storedIdsCopy, function(idx, storedFileId) { + self.cancel(storedFileId); + }); + + this._handler.cancelAll(); + }, + + clearStoredFiles: function() { + this._storedIds = []; + }, + + continueUpload: function(id) { + var uploadData = this._uploadData.retrieve({id: id}); + + if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { + return false; + } + + if (uploadData.status === qq.status.PAUSED) { + this.log(qq.format("Paused file ID {} ({}) will be continued. Not paused.", id, this.getName(id))); + this._uploadFile(id); + return true; + } + else { + this.log(qq.format("Ignoring continue for file ID {} ({}). Not paused.", id, this.getName(id)), "error"); + } + + return false; + }, + + deleteFile: function(id) { + return this._onSubmitDelete(id); + }, + + // TODO document? + doesExist: function(fileOrBlobId) { + return this._handler.isValid(fileOrBlobId); + }, + + // Generate a variable size thumbnail on an img or canvas, + // returning a promise that is fulfilled when the attempt completes. + // Thumbnail can either be based off of a URL for an image returned + // by the server in the upload response, or the associated `Blob`. + drawThumbnail: function(fileId, imgOrCanvas, maxSize, fromServer) { + var promiseToReturn = new qq.Promise(), + fileOrUrl, options; + + if (this._imageGenerator) { + fileOrUrl = this._thumbnailUrls[fileId]; + options = { + scale: maxSize > 0, + maxSize: maxSize > 0 ? maxSize : null + }; + + // If client-side preview generation is possible + // and we are not specifically looking for the image URl returned by the server... + if (!fromServer && qq.supportedFeatures.imagePreviews) { + fileOrUrl = this.getFile(fileId); + } + + /* jshint eqeqeq:false,eqnull:true */ + if (fileOrUrl == null) { + promiseToReturn.failure({container: imgOrCanvas, error: "File or URL not found."}); + } + else { + this._imageGenerator.generate(fileOrUrl, imgOrCanvas, options).then( + function success(modifiedContainer) { + promiseToReturn.success(modifiedContainer); + }, + + function failure(container, reason) { + promiseToReturn.failure({container: container, error: reason || "Problem generating thumbnail"}); + } + ); + } + } + else { + promiseToReturn.failure({container: imgOrCanvas, error: "Missing image generator module"}); + } + + return promiseToReturn; + }, + + getButton: function(fileId) { + return this._getButton(this._buttonIdsForFileIds[fileId]); + }, + + getEndpoint: function(fileId) { + return this._endpointStore.get(fileId); + }, + + getFile: function(fileOrBlobId) { + return this._handler.getFile(fileOrBlobId) || null; + }, + + getInProgress: function() { + return this._uploadData.retrieve({ + status: [ + qq.status.UPLOADING, + qq.status.UPLOAD_RETRYING, + qq.status.QUEUED + ] + }).length; + }, + + getName: function(id) { + return this._uploadData.retrieve({id: id}).name; + }, + + // Parent ID for a specific file, or null if this is the parent, or if it has no parent. + getParentId: function(id) { + var uploadDataEntry = this.getUploads({id: id}), + parentId = null; + + if (uploadDataEntry) { + if (uploadDataEntry.parentId !== undefined) { + parentId = uploadDataEntry.parentId; + } + } + + return parentId; + }, + + getResumableFilesData: function() { + return this._handler.getResumableFilesData(); + }, + + getSize: function(id) { + return this._uploadData.retrieve({id: id}).size; + }, + + getNetUploads: function() { + return this._netUploaded; + }, + + getRemainingAllowedItems: function() { + var allowedItems = this._currentItemLimit; + + if (allowedItems > 0) { + return allowedItems - this._netUploadedOrQueued; + } + + return null; + }, + + getUploads: function(optionalFilter) { + return this._uploadData.retrieve(optionalFilter); + }, + + getUuid: function(id) { + return this._uploadData.retrieve({id: id}).uuid; + }, + + log: function(str, level) { + if (this._options.debug && (!level || level === "info")) { + qq.log("[Fine Uploader " + qq.version + "] " + str); + } + else if (level && level !== "info") { + qq.log("[Fine Uploader " + qq.version + "] " + str, level); + + } + }, + + pauseUpload: function(id) { + var uploadData = this._uploadData.retrieve({id: id}); + + if (!qq.supportedFeatures.pause || !this._options.chunking.enabled) { + return false; + } + + // Pause only really makes sense if the file is uploading or retrying + if (qq.indexOf([qq.status.UPLOADING, qq.status.UPLOAD_RETRYING], uploadData.status) >= 0) { + if (this._handler.pause(id)) { + this._uploadData.setStatus(id, qq.status.PAUSED); + return true; + } + else { + this.log(qq.format("Unable to pause file ID {} ({}).", id, this.getName(id)), "error"); + } + } + else { + this.log(qq.format("Ignoring pause for file ID {} ({}). Not in progress.", id, this.getName(id)), "error"); + } + + return false; + }, + + reset: function() { + this.log("Resetting uploader..."); + + this._handler.reset(); + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._thumbnailUrls = []; + + qq.each(this._buttons, function(idx, button) { + button.reset(); + }); + + this._paramsStore.reset(); + this._endpointStore.reset(); + this._netUploadedOrQueued = 0; + this._netUploaded = 0; + this._uploadData.reset(); + this._buttonIdsForFileIds = []; + + this._pasteHandler && this._pasteHandler.reset(); + this._options.session.refreshOnReset && this._refreshSessionData(); + + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + + this._totalProgress && this._totalProgress.reset(); + }, + + retry: function(id) { + return this._manualRetry(id); + }, + + scaleImage: function(id, specs) { + var self = this; + + return qq.Scaler.prototype.scaleImage(id, specs, { + log: qq.bind(self.log, self), + getFile: qq.bind(self.getFile, self), + uploadData: self._uploadData + }); + }, + + setCustomHeaders: function(headers, id) { + this._customHeadersStore.set(headers, id); + }, + + setDeleteFileCustomHeaders: function(headers, id) { + this._deleteFileCustomHeadersStore.set(headers, id); + }, + + setDeleteFileEndpoint: function(endpoint, id) { + this._deleteFileEndpointStore.set(endpoint, id); + }, + + setDeleteFileParams: function(params, id) { + this._deleteFileParamsStore.set(params, id); + }, + + // Re-sets the default endpoint, an endpoint for a specific file, or an endpoint for a specific button + setEndpoint: function(endpoint, id) { + this._endpointStore.set(endpoint, id); + }, + + setForm: function(elementOrId) { + this._updateFormSupportAndParams(elementOrId); + }, + + setItemLimit: function(newItemLimit) { + this._currentItemLimit = newItemLimit; + }, + + setName: function(id, newName) { + this._uploadData.updateName(id, newName); + }, + + setParams: function(params, id) { + this._paramsStore.set(params, id); + }, + + setUuid: function(id, newUuid) { + return this._uploadData.uuidChanged(id, newUuid); + }, + + uploadStoredFiles: function() { + if (this._storedIds.length === 0) { + this._itemError("noFilesError"); + } + else { + this._uploadStoredFiles(); + } + } + }; + + /** + * Defines the private (internal) API for FineUploaderBasic mode. + */ + qq.basePrivateApi = { + // Updates internal state with a file record (not backed by a live file). Returns the assigned ID. + _addCannedFile: function(sessionData) { + var id = this._uploadData.addFile({ + uuid: sessionData.uuid, + name: sessionData.name, + size: sessionData.size, + status: qq.status.UPLOAD_SUCCESSFUL + }); + + sessionData.deleteFileEndpoint && this.setDeleteFileEndpoint(sessionData.deleteFileEndpoint, id); + sessionData.deleteFileParams && this.setDeleteFileParams(sessionData.deleteFileParams, id); + + if (sessionData.thumbnailUrl) { + this._thumbnailUrls[id] = sessionData.thumbnailUrl; + } + + this._netUploaded++; + this._netUploadedOrQueued++; + + return id; + }, + + _annotateWithButtonId: function(file, associatedInput) { + if (qq.isFile(file)) { + file.qqButtonId = this._getButtonId(associatedInput); + } + }, + + _batchError: function(message) { + this._options.callbacks.onError(null, null, message, undefined); + }, + + _createDeleteHandler: function() { + var self = this; + + return new qq.DeleteFileAjaxRequester({ + method: this._options.deleteFile.method.toUpperCase(), + maxConnections: this._options.maxConnections, + uuidParamName: this._options.request.uuidName, + customHeaders: this._deleteFileCustomHeadersStore, + paramsStore: this._deleteFileParamsStore, + endpointStore: this._deleteFileEndpointStore, + cors: this._options.cors, + log: qq.bind(self.log, self), + onDelete: function(id) { + self._onDelete(id); + self._options.callbacks.onDelete(id); + }, + onDeleteComplete: function(id, xhrOrXdr, isError) { + self._onDeleteComplete(id, xhrOrXdr, isError); + self._options.callbacks.onDeleteComplete(id, xhrOrXdr, isError); + } + + }); + }, + + _createPasteHandler: function() { + var self = this; + + return new qq.PasteSupport({ + targetElement: this._options.paste.targetElement, + callbacks: { + log: qq.bind(self.log, self), + pasteReceived: function(blob) { + self._handleCheckedCallback({ + name: "onPasteReceived", + callback: qq.bind(self._options.callbacks.onPasteReceived, self, blob), + onSuccess: qq.bind(self._handlePasteSuccess, self, blob), + identifier: "pasted image" + }); + } + } + }); + }, + + _createStore: function(initialValue, _readOnlyValues_) { + var store = {}, + catchall = initialValue, + perIdReadOnlyValues = {}, + readOnlyValues = _readOnlyValues_, + copy = function(orig) { + if (qq.isObject(orig)) { + return qq.extend({}, orig); + } + return orig; + }, + getReadOnlyValues = function() { + if (qq.isFunction(readOnlyValues)) { + return readOnlyValues(); + } + return readOnlyValues; + }, + includeReadOnlyValues = function(id, existing) { + if (readOnlyValues && qq.isObject(existing)) { + qq.extend(existing, getReadOnlyValues()); + } + + if (perIdReadOnlyValues[id]) { + qq.extend(existing, perIdReadOnlyValues[id]); + } + }; + + return { + set: function(val, id) { + /*jshint eqeqeq: true, eqnull: true*/ + if (id == null) { + store = {}; + catchall = copy(val); + } + else { + store[id] = copy(val); + } + }, + + get: function(id) { + var values; + + /*jshint eqeqeq: true, eqnull: true*/ + if (id != null && store[id]) { + values = store[id]; + } + else { + values = copy(catchall); + } + + includeReadOnlyValues(id, values); + + return copy(values); + }, + + addReadOnly: function(id, values) { + // Only applicable to Object stores + if (qq.isObject(store)) { + // If null ID, apply readonly values to all files + if (id === null) { + if (qq.isFunction(values)) { + readOnlyValues = values; + } + else { + readOnlyValues = readOnlyValues || {}; + qq.extend(readOnlyValues, values); + } + } + else { + perIdReadOnlyValues[id] = perIdReadOnlyValues[id] || {}; + qq.extend(perIdReadOnlyValues[id], values); + } + } + }, + + remove: function(fileId) { + return delete store[fileId]; + }, + + reset: function() { + store = {}; + perIdReadOnlyValues = {}; + catchall = initialValue; + } + }; + }, + + _createUploadDataTracker: function() { + var self = this; + + return new qq.UploadData({ + getName: function(id) { + return self.getName(id); + }, + getUuid: function(id) { + return self.getUuid(id); + }, + getSize: function(id) { + return self.getSize(id); + }, + onStatusChange: function(id, oldStatus, newStatus) { + self._onUploadStatusChange(id, oldStatus, newStatus); + self._options.callbacks.onStatusChange(id, oldStatus, newStatus); + self._maybeAllComplete(id, newStatus); + + if (self._totalProgress) { + setTimeout(function() { + self._totalProgress.onStatusChange(id, oldStatus, newStatus); + }, 0); + } + } + }); + }, + + /** + * Generate a tracked upload button. + * + * @param spec Object containing a required `element` property + * along with optional `multiple`, `accept`, and `folders`. + * @returns {qq.UploadButton} + * @private + */ + _createUploadButton: function(spec) { + var self = this, + acceptFiles = spec.accept || this._options.validation.acceptFiles, + allowedExtensions = spec.allowedExtensions || this._options.validation.allowedExtensions, + button; + + function allowMultiple() { + if (qq.supportedFeatures.ajaxUploading) { + // Workaround for bug in iOS7+ (see #1039) + if (self._options.workarounds.iosEmptyVideos && + qq.ios() && + !qq.ios6() && + self._isAllowedExtension(allowedExtensions, ".mov")) { + + return false; + } + + if (spec.multiple === undefined) { + return self._options.multiple; + } + + return spec.multiple; + } + + return false; + } + + button = new qq.UploadButton({ + element: spec.element, + folders: spec.folders, + name: this._options.request.inputName, + multiple: allowMultiple(), + acceptFiles: acceptFiles, + onChange: function(input) { + self._onInputChange(input); + }, + hoverClass: this._options.classes.buttonHover, + focusClass: this._options.classes.buttonFocus, + ios8BrowserCrashWorkaround: this._options.workarounds.ios8BrowserCrash + }); + + this._disposeSupport.addDisposer(function() { + button.dispose(); + }); + + self._buttons.push(button); + + return button; + }, + + _createUploadHandler: function(additionalOptions, namespace) { + var self = this, + lastOnProgress = {}, + options = { + debug: this._options.debug, + maxConnections: this._options.maxConnections, + cors: this._options.cors, + paramsStore: this._paramsStore, + endpointStore: this._endpointStore, + chunking: this._options.chunking, + resume: this._options.resume, + blobs: this._options.blobs, + log: qq.bind(self.log, self), + preventRetryParam: this._options.retry.preventRetryResponseProperty, + onProgress: function(id, name, loaded, total) { + if (loaded < 0 || total < 0) { + return; + } + + if (lastOnProgress[id]) { + if (lastOnProgress[id].loaded !== loaded || lastOnProgress[id].total !== total) { + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + } + } + else { + self._onProgress(id, name, loaded, total); + self._options.callbacks.onProgress(id, name, loaded, total); + } + + lastOnProgress[id] = {loaded: loaded, total: total}; + + }, + onComplete: function(id, name, result, xhr) { + delete lastOnProgress[id]; + + var status = self.getUploads({id: id}).status, + retVal; + + // This is to deal with some observed cases where the XHR readyStateChange handler is + // invoked by the browser multiple times for the same XHR instance with the same state + // readyState value. Higher level: don't invoke complete-related code if we've already + // done this. + if (status === qq.status.UPLOAD_SUCCESSFUL || status === qq.status.UPLOAD_FAILED) { + return; + } + + retVal = self._onComplete(id, name, result, xhr); + + // If the internal `_onComplete` handler returns a promise, don't invoke the `onComplete` callback + // until the promise has been fulfilled. + if (retVal instanceof qq.Promise) { + retVal.done(function() { + self._options.callbacks.onComplete(id, name, result, xhr); + }); + } + else { + self._options.callbacks.onComplete(id, name, result, xhr); + } + }, + onCancel: function(id, name, cancelFinalizationEffort) { + var promise = new qq.Promise(); + + self._handleCheckedCallback({ + name: "onCancel", + callback: qq.bind(self._options.callbacks.onCancel, self, id, name), + onFailure: promise.failure, + onSuccess: function() { + cancelFinalizationEffort.then(function() { + self._onCancel(id, name); + }); + + promise.success(); + }, + identifier: id + }); + + return promise; + }, + onUploadPrep: qq.bind(this._onUploadPrep, this), + onUpload: function(id, name) { + self._onUpload(id, name); + self._options.callbacks.onUpload(id, name); + }, + onUploadChunk: function(id, name, chunkData) { + self._onUploadChunk(id, chunkData); + self._options.callbacks.onUploadChunk(id, name, chunkData); + }, + onUploadChunkSuccess: function(id, chunkData, result, xhr) { + self._options.callbacks.onUploadChunkSuccess.apply(self, arguments); + }, + onResume: function(id, name, chunkData) { + return self._options.callbacks.onResume(id, name, chunkData); + }, + onAutoRetry: function(id, name, responseJSON, xhr) { + return self._onAutoRetry.apply(self, arguments); + }, + onUuidChanged: function(id, newUuid) { + self.log("Server requested UUID change from '" + self.getUuid(id) + "' to '" + newUuid + "'"); + self.setUuid(id, newUuid); + }, + getName: qq.bind(self.getName, self), + getUuid: qq.bind(self.getUuid, self), + getSize: qq.bind(self.getSize, self), + setSize: qq.bind(self._setSize, self), + getDataByUuid: function(uuid) { + return self.getUploads({uuid: uuid}); + }, + isQueued: function(id) { + var status = self.getUploads({id: id}).status; + return status === qq.status.QUEUED || + status === qq.status.SUBMITTED || + status === qq.status.UPLOAD_RETRYING || + status === qq.status.PAUSED; + }, + getIdsInProxyGroup: self._uploadData.getIdsInProxyGroup, + getIdsInBatch: self._uploadData.getIdsInBatch + }; + + qq.each(this._options.request, function(prop, val) { + options[prop] = val; + }); + + options.customHeaders = this._customHeadersStore; + + if (additionalOptions) { + qq.each(additionalOptions, function(key, val) { + options[key] = val; + }); + } + + return new qq.UploadHandlerController(options, namespace); + }, + + _fileOrBlobRejected: function(id) { + this._netUploadedOrQueued--; + this._uploadData.setStatus(id, qq.status.REJECTED); + }, + + _formatSize: function(bytes) { + var i = -1; + do { + bytes = bytes / 1000; + i++; + } while (bytes > 999); + + return Math.max(bytes, 0.1).toFixed(1) + this._options.text.sizeSymbols[i]; + }, + + // Creates an internal object that tracks various properties of each extra button, + // and then actually creates the extra button. + _generateExtraButtonSpecs: function() { + var self = this; + + this._extraButtonSpecs = {}; + + qq.each(this._options.extraButtons, function(idx, extraButtonOptionEntry) { + var multiple = extraButtonOptionEntry.multiple, + validation = qq.extend({}, self._options.validation, true), + extraButtonSpec = qq.extend({}, extraButtonOptionEntry); + + if (multiple === undefined) { + multiple = self._options.multiple; + } + + if (extraButtonSpec.validation) { + qq.extend(validation, extraButtonOptionEntry.validation, true); + } + + qq.extend(extraButtonSpec, { + multiple: multiple, + validation: validation + }, true); + + self._initExtraButton(extraButtonSpec); + }); + }, + + _getButton: function(buttonId) { + var extraButtonsSpec = this._extraButtonSpecs[buttonId]; + + if (extraButtonsSpec) { + return extraButtonsSpec.element; + } + else if (buttonId === this._defaultButtonId) { + return this._options.button; + } + }, + + /** + * Gets the internally used tracking ID for a button. + * + * @param buttonOrFileInputOrFile `File`, ``, or a button container element + * @returns {*} The button's ID, or undefined if no ID is recoverable + * @private + */ + _getButtonId: function(buttonOrFileInputOrFile) { + var inputs, fileInput, + fileBlobOrInput = buttonOrFileInputOrFile; + + // We want the reference file/blob here if this is a proxy (a file that will be generated on-demand later) + if (fileBlobOrInput instanceof qq.BlobProxy) { + fileBlobOrInput = fileBlobOrInput.referenceBlob; + } + + // If the item is a `Blob` it will never be associated with a button or drop zone. + if (fileBlobOrInput && !qq.isBlob(fileBlobOrInput)) { + if (qq.isFile(fileBlobOrInput)) { + return fileBlobOrInput.qqButtonId; + } + else if (fileBlobOrInput.tagName.toLowerCase() === "input" && + fileBlobOrInput.type.toLowerCase() === "file") { + + return fileBlobOrInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); + } + + inputs = fileBlobOrInput.getElementsByTagName("input"); + + qq.each(inputs, function(idx, input) { + if (input.getAttribute("type") === "file") { + fileInput = input; + return false; + } + }); + + if (fileInput) { + return fileInput.getAttribute(qq.UploadButton.BUTTON_ID_ATTR_NAME); + } + } + }, + + _getNotFinished: function() { + return this._uploadData.retrieve({ + status: [ + qq.status.UPLOADING, + qq.status.UPLOAD_RETRYING, + qq.status.QUEUED, + qq.status.SUBMITTING, + qq.status.SUBMITTED, + qq.status.PAUSED + ] + }).length; + }, + + // Get the validation options for this button. Could be the default validation option + // or a specific one assigned to this particular button. + _getValidationBase: function(buttonId) { + var extraButtonSpec = this._extraButtonSpecs[buttonId]; + + return extraButtonSpec ? extraButtonSpec.validation : this._options.validation; + }, + + _getValidationDescriptor: function(fileWrapper) { + if (fileWrapper.file instanceof qq.BlobProxy) { + return { + name: qq.getFilename(fileWrapper.file.referenceBlob), + size: fileWrapper.file.referenceBlob.size + }; + } + + return { + name: this.getUploads({id: fileWrapper.id}).name, + size: this.getUploads({id: fileWrapper.id}).size + }; + }, + + _getValidationDescriptors: function(fileWrappers) { + var self = this, + fileDescriptors = []; + + qq.each(fileWrappers, function(idx, fileWrapper) { + fileDescriptors.push(self._getValidationDescriptor(fileWrapper)); + }); + + return fileDescriptors; + }, + + // Allows camera access on either the default or an extra button for iOS devices. + _handleCameraAccess: function() { + if (this._options.camera.ios && qq.ios()) { + var acceptIosCamera = "image/*;capture=camera", + button = this._options.camera.button, + buttonId = button ? this._getButtonId(button) : this._defaultButtonId, + optionRoot = this._options; + + // If we are not targeting the default button, it is an "extra" button + if (buttonId && buttonId !== this._defaultButtonId) { + optionRoot = this._extraButtonSpecs[buttonId]; + } + + // Camera access won't work in iOS if the `multiple` attribute is present on the file input + optionRoot.multiple = false; + + // update the options + if (optionRoot.validation.acceptFiles === null) { + optionRoot.validation.acceptFiles = acceptIosCamera; + } + else { + optionRoot.validation.acceptFiles += "," + acceptIosCamera; + } + + // update the already-created button + qq.each(this._buttons, function(idx, button) { + if (button.getButtonId() === buttonId) { + button.setMultiple(optionRoot.multiple); + button.setAcceptFiles(optionRoot.acceptFiles); + + return false; + } + }); + } + }, + + _handleCheckedCallback: function(details) { + var self = this, + callbackRetVal = details.callback(); + + if (qq.isGenericPromise(callbackRetVal)) { + this.log(details.name + " - waiting for " + details.name + " promise to be fulfilled for " + details.identifier); + return callbackRetVal.then( + function(successParam) { + self.log(details.name + " promise success for " + details.identifier); + details.onSuccess(successParam); + }, + function() { + if (details.onFailure) { + self.log(details.name + " promise failure for " + details.identifier); + details.onFailure(); + } + else { + self.log(details.name + " promise failure for " + details.identifier); + } + }); + } + + if (callbackRetVal !== false) { + details.onSuccess(callbackRetVal); + } + else { + if (details.onFailure) { + this.log(details.name + " - return value was 'false' for " + details.identifier + ". Invoking failure callback."); + details.onFailure(); + } + else { + this.log(details.name + " - return value was 'false' for " + details.identifier + ". Will not proceed."); + } + } + + return callbackRetVal; + }, + + // Updates internal state when a new file has been received, and adds it along with its ID to a passed array. + _handleNewFile: function(file, batchId, newFileWrapperList) { + var self = this, + uuid = qq.getUniqueId(), + size = -1, + name = qq.getFilename(file), + actualFile = file.blob || file, + handler = this._customNewFileHandler ? + this._customNewFileHandler : + qq.bind(self._handleNewFileGeneric, self); + + if (!qq.isInput(actualFile) && actualFile.size >= 0) { + size = actualFile.size; + } + + handler(actualFile, name, uuid, size, newFileWrapperList, batchId, this._options.request.uuidName, { + uploadData: self._uploadData, + paramsStore: self._paramsStore, + addFileToHandler: function(id, file) { + self._handler.add(id, file); + self._netUploadedOrQueued++; + self._trackButton(id); + } + }); + }, + + _handleNewFileGeneric: function(file, name, uuid, size, fileList, batchId) { + var id = this._uploadData.addFile({uuid: uuid, name: name, size: size, batchId: batchId}); + + this._handler.add(id, file); + this._trackButton(id); + + this._netUploadedOrQueued++; + + fileList.push({id: id, file: file}); + }, + + _handlePasteSuccess: function(blob, extSuppliedName) { + var extension = blob.type.split("/")[1], + name = extSuppliedName; + + /*jshint eqeqeq: true, eqnull: true*/ + if (name == null) { + name = this._options.paste.defaultName; + } + + name += "." + extension; + + this.addFiles({ + name: name, + blob: blob + }); + }, + + // Creates an extra button element + _initExtraButton: function(spec) { + var button = this._createUploadButton({ + element: spec.element, + multiple: spec.multiple, + accept: spec.validation.acceptFiles, + folders: spec.folders, + allowedExtensions: spec.validation.allowedExtensions + }); + + this._extraButtonSpecs[button.getButtonId()] = spec; + }, + + _initFormSupportAndParams: function() { + this._formSupport = qq.FormSupport && new qq.FormSupport( + this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this) + ); + + if (this._formSupport && this._formSupport.attachedToForm) { + this._paramsStore = this._createStore( + this._options.request.params, this._formSupport.getFormInputsAsObject + ); + + this._options.autoUpload = this._formSupport.newAutoUpload; + if (this._formSupport.newEndpoint) { + this._options.request.endpoint = this._formSupport.newEndpoint; + } + } + else { + this._paramsStore = this._createStore(this._options.request.params); + } + }, + + _isDeletePossible: function() { + if (!qq.DeleteFileAjaxRequester || !this._options.deleteFile.enabled) { + return false; + } + + if (this._options.cors.expected) { + if (qq.supportedFeatures.deleteFileCorsXhr) { + return true; + } + + if (qq.supportedFeatures.deleteFileCorsXdr && this._options.cors.allowXdr) { + return true; + } + + return false; + } + + return true; + }, + + _isAllowedExtension: function(allowed, fileName) { + var valid = false; + + if (!allowed.length) { + return true; + } + + qq.each(allowed, function(idx, allowedExt) { + /** + * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the + * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details. + */ + if (qq.isString(allowedExt)) { + /*jshint eqeqeq: true, eqnull: true*/ + var extRegex = new RegExp("\\." + allowedExt + "$", "i"); + + if (fileName.match(extRegex) != null) { + valid = true; + return false; + } + } + }); + + return valid; + }, + + /** + * Constructs and returns a message that describes an item/file error. Also calls `onError` callback. + * + * @param code REQUIRED - a code that corresponds to a stock message describing this type of error + * @param maybeNameOrNames names of the items that have failed, if applicable + * @param item `File`, `Blob`, or `` + * @private + */ + _itemError: function(code, maybeNameOrNames, item) { + var message = this._options.messages[code], + allowedExtensions = [], + names = [].concat(maybeNameOrNames), + name = names[0], + buttonId = this._getButtonId(item), + validationBase = this._getValidationBase(buttonId), + extensionsForMessage, placeholderMatch; + + function r(name, replacement) { message = message.replace(name, replacement); } + + qq.each(validationBase.allowedExtensions, function(idx, allowedExtension) { + /** + * If an argument is not a string, ignore it. Added when a possible issue with MooTools hijacking the + * `allowedExtensions` array was discovered. See case #735 in the issue tracker for more details. + */ + if (qq.isString(allowedExtension)) { + allowedExtensions.push(allowedExtension); + } + }); + + extensionsForMessage = allowedExtensions.join(", ").toLowerCase(); + + r("{file}", this._options.formatFileName(name)); + r("{extensions}", extensionsForMessage); + r("{sizeLimit}", this._formatSize(validationBase.sizeLimit)); + r("{minSizeLimit}", this._formatSize(validationBase.minSizeLimit)); + + placeholderMatch = message.match(/(\{\w+\})/g); + if (placeholderMatch !== null) { + qq.each(placeholderMatch, function(idx, placeholder) { + r(placeholder, names[idx]); + }); + } + + this._options.callbacks.onError(null, name, message, undefined); + + return message; + }, + + /** + * Conditionally orders a manual retry of a failed upload. + * + * @param id File ID of the failed upload + * @param callback Optional callback to invoke if a retry is prudent. + * In lieu of asking the upload handler to retry. + * @returns {boolean} true if a manual retry will occur + * @private + */ + _manualRetry: function(id, callback) { + if (this._onBeforeManualRetry(id)) { + this._netUploadedOrQueued++; + this._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); + + if (callback) { + callback(id); + } + else { + this._handler.retry(id); + } + + return true; + } + }, + + _maybeAllComplete: function(id, status) { + var self = this, + notFinished = this._getNotFinished(); + + if (status === qq.status.UPLOAD_SUCCESSFUL) { + this._succeededSinceLastAllComplete.push(id); + } + else if (status === qq.status.UPLOAD_FAILED) { + this._failedSinceLastAllComplete.push(id); + } + + if (notFinished === 0 && + (this._succeededSinceLastAllComplete.length || this._failedSinceLastAllComplete.length)) { + // Attempt to ensure onAllComplete is not invoked before other callbacks, such as onCancel & onComplete + setTimeout(function() { + self._onAllComplete(self._succeededSinceLastAllComplete, self._failedSinceLastAllComplete); + }, 0); + } + }, + + _maybeHandleIos8SafariWorkaround: function() { + var self = this; + + if (this._options.workarounds.ios8SafariUploads && qq.ios800() && qq.iosSafari()) { + setTimeout(function() { + window.alert(self._options.messages.unsupportedBrowserIos8Safari); + }, 0); + throw new qq.Error(this._options.messages.unsupportedBrowserIos8Safari); + } + }, + + _maybeParseAndSendUploadError: function(id, name, response, xhr) { + // Assuming no one will actually set the response code to something other than 200 + // and still set 'success' to true... + if (!response.success) { + if (xhr && xhr.status !== 200 && !response.error) { + this._options.callbacks.onError(id, name, "XHR returned response code " + xhr.status, xhr); + } + else { + var errorReason = response.error ? response.error : this._options.text.defaultResponseError; + this._options.callbacks.onError(id, name, errorReason, xhr); + } + } + }, + + _maybeProcessNextItemAfterOnValidateCallback: function(validItem, items, index, params, endpoint) { + var self = this; + + if (items.length > index) { + if (validItem || !this._options.validation.stopOnFirstInvalidFile) { + //use setTimeout to prevent a stack overflow with a large number of files in the batch & non-promissory callbacks + setTimeout(function() { + var validationDescriptor = self._getValidationDescriptor(items[index]), + buttonId = self._getButtonId(items[index].file), + button = self._getButton(buttonId); + + self._handleCheckedCallback({ + name: "onValidate", + callback: qq.bind(self._options.callbacks.onValidate, self, validationDescriptor, button), + onSuccess: qq.bind(self._onValidateCallbackSuccess, self, items, index, params, endpoint), + onFailure: qq.bind(self._onValidateCallbackFailure, self, items, index, params, endpoint), + identifier: "Item '" + validationDescriptor.name + "', size: " + validationDescriptor.size + }); + }, 0); + } + else if (!validItem) { + for (; index < items.length; index++) { + self._fileOrBlobRejected(items[index].id); + } + } + } + }, + + _onAllComplete: function(successful, failed) { + this._totalProgress && this._totalProgress.onAllComplete(successful, failed, this._preventRetries); + + this._options.callbacks.onAllComplete(qq.extend([], successful), qq.extend([], failed)); + + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + }, + + /** + * Attempt to automatically retry a failed upload. + * + * @param id The file ID of the failed upload + * @param name The name of the file associated with the failed upload + * @param responseJSON Response from the server, parsed into a javascript object + * @param xhr Ajax transport used to send the failed request + * @param callback Optional callback to be invoked if a retry is prudent. + * Invoked in lieu of asking the upload handler to retry. + * @returns {boolean} true if an auto-retry will occur + * @private + */ + _onAutoRetry: function(id, name, responseJSON, xhr, callback) { + var self = this; + + self._preventRetries[id] = responseJSON[self._options.retry.preventRetryResponseProperty]; + + if (self._shouldAutoRetry(id, name, responseJSON)) { + self._maybeParseAndSendUploadError.apply(self, arguments); + self._options.callbacks.onAutoRetry(id, name, self._autoRetries[id]); + self._onBeforeAutoRetry(id, name); + + self._retryTimeouts[id] = setTimeout(function() { + self.log("Retrying " + name + "..."); + self._uploadData.setStatus(id, qq.status.UPLOAD_RETRYING); + + if (callback) { + callback(id); + } + else { + self._handler.retry(id); + } + }, self._options.retry.autoAttemptDelay * 1000); + + return true; + } + }, + + _onBeforeAutoRetry: function(id, name) { + this.log("Waiting " + this._options.retry.autoAttemptDelay + " seconds before retrying " + name + "..."); + }, + + //return false if we should not attempt the requested retry + _onBeforeManualRetry: function(id) { + var itemLimit = this._currentItemLimit, + fileName; + + if (this._preventRetries[id]) { + this.log("Retries are forbidden for id " + id, "warn"); + return false; + } + else if (this._handler.isValid(id)) { + fileName = this.getName(id); + + if (this._options.callbacks.onManualRetry(id, fileName) === false) { + return false; + } + + if (itemLimit > 0 && this._netUploadedOrQueued + 1 > itemLimit) { + this._itemError("retryFailTooManyItems"); + return false; + } + + this.log("Retrying upload for '" + fileName + "' (id: " + id + ")..."); + return true; + } + else { + this.log("'" + id + "' is not a valid file ID", "error"); + return false; + } + }, + + _onCancel: function(id, name) { + this._netUploadedOrQueued--; + + clearTimeout(this._retryTimeouts[id]); + + var storedItemIndex = qq.indexOf(this._storedIds, id); + if (!this._options.autoUpload && storedItemIndex >= 0) { + this._storedIds.splice(storedItemIndex, 1); + } + + this._uploadData.setStatus(id, qq.status.CANCELED); + }, + + _onComplete: function(id, name, result, xhr) { + if (!result.success) { + this._netUploadedOrQueued--; + this._uploadData.setStatus(id, qq.status.UPLOAD_FAILED); + + if (result[this._options.retry.preventRetryResponseProperty] === true) { + this._preventRetries[id] = true; + } + } + else { + if (result.thumbnailUrl) { + this._thumbnailUrls[id] = result.thumbnailUrl; + } + + this._netUploaded++; + this._uploadData.setStatus(id, qq.status.UPLOAD_SUCCESSFUL); + } + + this._maybeParseAndSendUploadError(id, name, result, xhr); + + return result.success ? true : false; + }, + + _onDelete: function(id) { + this._uploadData.setStatus(id, qq.status.DELETING); + }, + + _onDeleteComplete: function(id, xhrOrXdr, isError) { + var name = this.getName(id); + + if (isError) { + this._uploadData.setStatus(id, qq.status.DELETE_FAILED); + this.log("Delete request for '" + name + "' has failed.", "error"); + + // For error reporing, we only have accesss to the response status if this is not + // an `XDomainRequest`. + if (xhrOrXdr.withCredentials === undefined) { + this._options.callbacks.onError(id, name, "Delete request failed", xhrOrXdr); + } + else { + this._options.callbacks.onError(id, name, "Delete request failed with response code " + xhrOrXdr.status, xhrOrXdr); + } + } + else { + this._netUploadedOrQueued--; + this._netUploaded--; + this._handler.expunge(id); + this._uploadData.setStatus(id, qq.status.DELETED); + this.log("Delete request for '" + name + "' has succeeded."); + } + }, + + _onInputChange: function(input) { + var fileIndex; + + if (qq.supportedFeatures.ajaxUploading) { + for (fileIndex = 0; fileIndex < input.files.length; fileIndex++) { + this._annotateWithButtonId(input.files[fileIndex], input); + } + + this.addFiles(input.files); + } + // Android 2.3.x will fire `onchange` even if no file has been selected + else if (input.value.length > 0) { + this.addFiles(input); + } + + qq.each(this._buttons, function(idx, button) { + button.reset(); + }); + }, + + _onProgress: function(id, name, loaded, total) { + this._totalProgress && this._totalProgress.onIndividualProgress(id, loaded, total); + }, + + _onSubmit: function(id, name) { + //nothing to do yet in core uploader + }, + + _onSubmitCallbackSuccess: function(id, name) { + this._onSubmit.apply(this, arguments); + this._uploadData.setStatus(id, qq.status.SUBMITTED); + this._onSubmitted.apply(this, arguments); + + if (this._options.autoUpload) { + this._options.callbacks.onSubmitted.apply(this, arguments); + this._uploadFile(id); + } + else { + this._storeForLater(id); + this._options.callbacks.onSubmitted.apply(this, arguments); + } + }, + + _onSubmitDelete: function(id, onSuccessCallback, additionalMandatedParams) { + var uuid = this.getUuid(id), + adjustedOnSuccessCallback; + + if (onSuccessCallback) { + adjustedOnSuccessCallback = qq.bind(onSuccessCallback, this, id, uuid, additionalMandatedParams); + } + + if (this._isDeletePossible()) { + this._handleCheckedCallback({ + name: "onSubmitDelete", + callback: qq.bind(this._options.callbacks.onSubmitDelete, this, id), + onSuccess: adjustedOnSuccessCallback || + qq.bind(this._deleteHandler.sendDelete, this, id, uuid, additionalMandatedParams), + identifier: id + }); + return true; + } + else { + this.log("Delete request ignored for ID " + id + ", delete feature is disabled or request not possible " + + "due to CORS on a user agent that does not support pre-flighting.", "warn"); + return false; + } + }, + + _onSubmitted: function(id) { + //nothing to do in the base uploader + }, + + _onTotalProgress: function(loaded, total) { + this._options.callbacks.onTotalProgress(loaded, total); + }, + + _onUploadPrep: function(id) { + // nothing to do in the core uploader for now + }, + + _onUpload: function(id, name) { + this._uploadData.setStatus(id, qq.status.UPLOADING); + }, + + _onUploadChunk: function(id, chunkData) { + //nothing to do in the base uploader + }, + + _onUploadStatusChange: function(id, oldStatus, newStatus) { + // Make sure a "queued" retry attempt is canceled if the upload has been paused + if (newStatus === qq.status.PAUSED) { + clearTimeout(this._retryTimeouts[id]); + } + }, + + _onValidateBatchCallbackFailure: function(fileWrappers) { + var self = this; + + qq.each(fileWrappers, function(idx, fileWrapper) { + self._fileOrBlobRejected(fileWrapper.id); + }); + }, + + _onValidateBatchCallbackSuccess: function(validationDescriptors, items, params, endpoint, button) { + var errorMessage, + itemLimit = this._currentItemLimit, + proposedNetFilesUploadedOrQueued = this._netUploadedOrQueued; + + if (itemLimit === 0 || proposedNetFilesUploadedOrQueued <= itemLimit) { + if (items.length > 0) { + this._handleCheckedCallback({ + name: "onValidate", + callback: qq.bind(this._options.callbacks.onValidate, this, validationDescriptors[0], button), + onSuccess: qq.bind(this._onValidateCallbackSuccess, this, items, 0, params, endpoint), + onFailure: qq.bind(this._onValidateCallbackFailure, this, items, 0, params, endpoint), + identifier: "Item '" + items[0].file.name + "', size: " + items[0].file.size + }); + } + else { + this._itemError("noFilesError"); + } + } + else { + this._onValidateBatchCallbackFailure(items); + errorMessage = this._options.messages.tooManyItemsError + .replace(/\{netItems\}/g, proposedNetFilesUploadedOrQueued) + .replace(/\{itemLimit\}/g, itemLimit); + this._batchError(errorMessage); + } + }, + + _onValidateCallbackFailure: function(items, index, params, endpoint) { + var nextIndex = index + 1; + + this._fileOrBlobRejected(items[index].id, items[index].file.name); + + this._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); + }, + + _onValidateCallbackSuccess: function(items, index, params, endpoint) { + var self = this, + nextIndex = index + 1, + validationDescriptor = this._getValidationDescriptor(items[index]); + + this._validateFileOrBlobData(items[index], validationDescriptor) + .then( + function() { + self._upload(items[index].id, params, endpoint); + self._maybeProcessNextItemAfterOnValidateCallback(true, items, nextIndex, params, endpoint); + }, + function() { + self._maybeProcessNextItemAfterOnValidateCallback(false, items, nextIndex, params, endpoint); + } + ); + }, + + _prepareItemsForUpload: function(items, params, endpoint) { + if (items.length === 0) { + this._itemError("noFilesError"); + return; + } + + var validationDescriptors = this._getValidationDescriptors(items), + buttonId = this._getButtonId(items[0].file), + button = this._getButton(buttonId); + + this._handleCheckedCallback({ + name: "onValidateBatch", + callback: qq.bind(this._options.callbacks.onValidateBatch, this, validationDescriptors, button), + onSuccess: qq.bind(this._onValidateBatchCallbackSuccess, this, validationDescriptors, items, params, endpoint, button), + onFailure: qq.bind(this._onValidateBatchCallbackFailure, this, items), + identifier: "batch validation" + }); + }, + + _preventLeaveInProgress: function() { + var self = this; + + this._disposeSupport.attach(window, "beforeunload", function(e) { + if (self.getInProgress()) { + e = e || window.event; + // for ie, ff + e.returnValue = self._options.messages.onLeave; + // for webkit + return self._options.messages.onLeave; + } + }); + }, + + // Attempts to refresh session data only if the `qq.Session` module exists + // and a session endpoint has been specified. The `onSessionRequestComplete` + // callback will be invoked once the refresh is complete. + _refreshSessionData: function() { + var self = this, + options = this._options.session; + + /* jshint eqnull:true */ + if (qq.Session && this._options.session.endpoint != null) { + if (!this._session) { + qq.extend(options, this._options.cors); + + options.log = qq.bind(this.log, this); + options.addFileRecord = qq.bind(this._addCannedFile, this); + + this._session = new qq.Session(options); + } + + setTimeout(function() { + self._session.refresh().then(function(response, xhrOrXdr) { + self._sessionRequestComplete(); + self._options.callbacks.onSessionRequestComplete(response, true, xhrOrXdr); + + }, function(response, xhrOrXdr) { + + self._options.callbacks.onSessionRequestComplete(response, false, xhrOrXdr); + }); + }, 0); + } + }, + + _sessionRequestComplete: function() {}, + + _setSize: function(id, newSize) { + this._uploadData.updateSize(id, newSize); + this._totalProgress && this._totalProgress.onNewSize(id); + }, + + _shouldAutoRetry: function(id, name, responseJSON) { + var uploadData = this._uploadData.retrieve({id: id}); + + /*jshint laxbreak: true */ + if (!this._preventRetries[id] + && this._options.retry.enableAuto + && uploadData.status !== qq.status.PAUSED) { + + if (this._autoRetries[id] === undefined) { + this._autoRetries[id] = 0; + } + + if (this._autoRetries[id] < this._options.retry.maxAutoAttempts) { + this._autoRetries[id] += 1; + return true; + } + } + + return false; + }, + + _storeForLater: function(id) { + this._storedIds.push(id); + }, + + // Maps a file with the button that was used to select it. + _trackButton: function(id) { + var buttonId; + + if (qq.supportedFeatures.ajaxUploading) { + buttonId = this._handler.getFile(id).qqButtonId; + } + else { + buttonId = this._getButtonId(this._handler.getInput(id)); + } + + if (buttonId) { + this._buttonIdsForFileIds[id] = buttonId; + } + }, + + _updateFormSupportAndParams: function(formElementOrId) { + this._options.form.element = formElementOrId; + + this._formSupport = qq.FormSupport && new qq.FormSupport( + this._options.form, qq.bind(this.uploadStoredFiles, this), qq.bind(this.log, this) + ); + + if (this._formSupport && this._formSupport.attachedToForm) { + this._paramsStore.addReadOnly(null, this._formSupport.getFormInputsAsObject); + + this._options.autoUpload = this._formSupport.newAutoUpload; + if (this._formSupport.newEndpoint) { + this.setEndpoint(this._formSupport.newEndpoint); + } + } + }, + + _upload: function(id, params, endpoint) { + var name = this.getName(id); + + if (params) { + this.setParams(params, id); + } + + if (endpoint) { + this.setEndpoint(endpoint, id); + } + + this._handleCheckedCallback({ + name: "onSubmit", + callback: qq.bind(this._options.callbacks.onSubmit, this, id, name), + onSuccess: qq.bind(this._onSubmitCallbackSuccess, this, id, name), + onFailure: qq.bind(this._fileOrBlobRejected, this, id, name), + identifier: id + }); + }, + + _uploadFile: function(id) { + if (!this._handler.upload(id)) { + this._uploadData.setStatus(id, qq.status.QUEUED); + } + }, + + _uploadStoredFiles: function() { + var idToUpload, stillSubmitting, + self = this; + + while (this._storedIds.length) { + idToUpload = this._storedIds.shift(); + this._uploadFile(idToUpload); + } + + // If we are still waiting for some files to clear validation, attempt to upload these again in a bit + stillSubmitting = this.getUploads({status: qq.status.SUBMITTING}).length; + if (stillSubmitting) { + qq.log("Still waiting for " + stillSubmitting + " files to clear submit queue. Will re-parse stored IDs array shortly."); + setTimeout(function() { + self._uploadStoredFiles(); + }, 1000); + } + }, + + /** + * Performs some internal validation checks on an item, defined in the `validation` option. + * + * @param fileWrapper Wrapper containing a `file` along with an `id` + * @param validationDescriptor Normalized information about the item (`size`, `name`). + * @returns qq.Promise with appropriate callbacks invoked depending on the validity of the file + * @private + */ + _validateFileOrBlobData: function(fileWrapper, validationDescriptor) { + var self = this, + file = (function() { + if (fileWrapper.file instanceof qq.BlobProxy) { + return fileWrapper.file.referenceBlob; + } + return fileWrapper.file; + }()), + name = validationDescriptor.name, + size = validationDescriptor.size, + buttonId = this._getButtonId(fileWrapper.file), + validationBase = this._getValidationBase(buttonId), + validityChecker = new qq.Promise(); + + validityChecker.then( + function() {}, + function() { + self._fileOrBlobRejected(fileWrapper.id, name); + }); + + if (qq.isFileOrInput(file) && !this._isAllowedExtension(validationBase.allowedExtensions, name)) { + this._itemError("typeError", name, file); + return validityChecker.failure(); + } + + if (size === 0) { + this._itemError("emptyError", name, file); + return validityChecker.failure(); + } + + if (size > 0 && validationBase.sizeLimit && size > validationBase.sizeLimit) { + this._itemError("sizeError", name, file); + return validityChecker.failure(); + } + + if (size > 0 && size < validationBase.minSizeLimit) { + this._itemError("minSizeError", name, file); + return validityChecker.failure(); + } + + if (qq.ImageValidation && qq.supportedFeatures.imagePreviews && qq.isFile(file)) { + new qq.ImageValidation(file, qq.bind(self.log, self)).validate(validationBase.image).then( + validityChecker.success, + function(errorCode) { + self._itemError(errorCode + "ImageError", name, file); + validityChecker.failure(); + } + ); + } + else { + validityChecker.success(); + } + + return validityChecker; + }, + + _wrapCallbacks: function() { + var self, safeCallback, prop; + + self = this; + + safeCallback = function(name, callback, args) { + var errorMsg; + + try { + return callback.apply(self, args); + } + catch (exception) { + errorMsg = exception.message || exception.toString(); + self.log("Caught exception in '" + name + "' callback - " + errorMsg, "error"); + } + }; + + /* jshint forin: false, loopfunc: true */ + for (prop in this._options.callbacks) { + (function() { + var callbackName, callbackFunc; + callbackName = prop; + callbackFunc = self._options.callbacks[callbackName]; + self._options.callbacks[callbackName] = function() { + return safeCallback(callbackName, callbackFunc, arguments); + }; + }()); + } + } + }; +}()); + +/*globals qq*/ +(function() { + "use strict"; + + qq.FineUploaderBasic = function(o) { + var self = this; + + // These options define FineUploaderBasic mode. + this._options = { + debug: false, + button: null, + multiple: true, + maxConnections: 3, + disableCancelForFormUploads: false, + autoUpload: true, + + request: { + customHeaders: {}, + endpoint: "/server/upload", + filenameParam: "qqfilename", + forceMultipart: true, + inputName: "qqfile", + method: "POST", + params: {}, + paramsInBody: true, + totalFileSizeName: "qqtotalfilesize", + uuidName: "qquuid" + }, + + validation: { + allowedExtensions: [], + sizeLimit: 0, + minSizeLimit: 0, + itemLimit: 0, + stopOnFirstInvalidFile: true, + acceptFiles: null, + image: { + maxHeight: 0, + maxWidth: 0, + minHeight: 0, + minWidth: 0 + } + }, + + callbacks: { + onSubmit: function(id, name) {}, + onSubmitted: function(id, name) {}, + onComplete: function(id, name, responseJSON, maybeXhr) {}, + onAllComplete: function(successful, failed) {}, + onCancel: function(id, name) {}, + onUpload: function(id, name) {}, + onUploadChunk: function(id, name, chunkData) {}, + onUploadChunkSuccess: function(id, chunkData, responseJSON, xhr) {}, + onResume: function(id, fileName, chunkData) {}, + onProgress: function(id, name, loaded, total) {}, + onTotalProgress: function(loaded, total) {}, + onError: function(id, name, reason, maybeXhrOrXdr) {}, + onAutoRetry: function(id, name, attemptNumber) {}, + onManualRetry: function(id, name) {}, + onValidateBatch: function(fileOrBlobData) {}, + onValidate: function(fileOrBlobData) {}, + onSubmitDelete: function(id) {}, + onDelete: function(id) {}, + onDeleteComplete: function(id, xhrOrXdr, isError) {}, + onPasteReceived: function(blob) {}, + onStatusChange: function(id, oldStatus, newStatus) {}, + onSessionRequestComplete: function(response, success, xhrOrXdr) {} + }, + + messages: { + typeError: "{file} has an invalid extension. Valid extension(s): {extensions}.", + sizeError: "{file} is too large, maximum file size is {sizeLimit}.", + minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", + emptyError: "{file} is empty, please select files again without it.", + noFilesError: "No files to upload.", + tooManyItemsError: "Too many items ({netItems}) would be uploaded. Item limit is {itemLimit}.", + maxHeightImageError: "Image is too tall.", + maxWidthImageError: "Image is too wide.", + minHeightImageError: "Image is not tall enough.", + minWidthImageError: "Image is not wide enough.", + retryFailTooManyItems: "Retry failed - you have reached your file limit.", + onLeave: "The files are being uploaded, if you leave now the upload will be canceled.", + unsupportedBrowserIos8Safari: "Unrecoverable error - this browser does not permit file uploading of any kind due to serious bugs in iOS8 Safari. Please use iOS8 Chrome until Apple fixes these issues." + }, + + retry: { + enableAuto: false, + maxAutoAttempts: 3, + autoAttemptDelay: 5, + preventRetryResponseProperty: "preventRetry" + }, + + classes: { + buttonHover: "qq-upload-button-hover", + buttonFocus: "qq-upload-button-focus" + }, + + chunking: { + enabled: false, + concurrent: { + enabled: false + }, + mandatory: false, + paramNames: { + partIndex: "qqpartindex", + partByteOffset: "qqpartbyteoffset", + chunkSize: "qqchunksize", + totalFileSize: "qqtotalfilesize", + totalParts: "qqtotalparts" + }, + partSize: 2000000, + // only relevant for traditional endpoints, only required when concurrent.enabled === true + success: { + endpoint: null + } + }, + + resume: { + enabled: false, + recordsExpireIn: 7, //days + paramNames: { + resuming: "qqresume" + } + }, + + formatFileName: function(fileOrBlobName) { + return fileOrBlobName; + }, + + text: { + defaultResponseError: "Upload failure reason unknown", + sizeSymbols: ["kB", "MB", "GB", "TB", "PB", "EB"] + }, + + deleteFile: { + enabled: false, + method: "DELETE", + endpoint: "/server/upload", + customHeaders: {}, + params: {} + }, + + cors: { + expected: false, + sendCredentials: false, + allowXdr: false + }, + + blobs: { + defaultName: "misc_data" + }, + + paste: { + targetElement: null, + defaultName: "pasted_image" + }, + + camera: { + ios: false, + + // if ios is true: button is null means target the default button, otherwise target the button specified + button: null + }, + + // This refers to additional upload buttons to be handled by Fine Uploader. + // Each element is an object, containing `element` as the only required + // property. The `element` must be a container that will ultimately + // contain an invisible `` created by Fine Uploader. + // Optional properties of each object include `multiple`, `validation`, + // and `folders`. + extraButtons: [], + + // Depends on the session module. Used to query the server for an initial file list + // during initialization and optionally after a `reset`. + session: { + endpoint: null, + params: {}, + customHeaders: {}, + refreshOnReset: true + }, + + // Send parameters associated with an existing form along with the files + form: { + // Element ID, HTMLElement, or null + element: "qq-form", + + // Overrides the base `autoUpload`, unless `element` is null. + autoUpload: false, + + // true = upload files on form submission (and squelch submit event) + interceptSubmit: true + }, + + // scale images client side, upload a new file for each scaled version + scaling: { + // send the original file as well + sendOriginal: true, + + // fox orientation for scaled images + orient: true, + + // If null, scaled image type will match reference image type. This value will be referred to + // for any size record that does not specific a type. + defaultType: null, + + defaultQuality: 80, + + failureText: "Failed to scale", + + includeExif: false, + + // metadata about each requested scaled version + sizes: [] + }, + + workarounds: { + iosEmptyVideos: true, + ios8SafariUploads: true, + ios8BrowserCrash: false + } + }; + + // Replace any default options with user defined ones + qq.extend(this._options, o, true); + + this._buttons = []; + this._extraButtonSpecs = {}; + this._buttonIdsForFileIds = []; + + this._wrapCallbacks(); + this._disposeSupport = new qq.DisposeSupport(); + + this._storedIds = []; + this._autoRetries = []; + this._retryTimeouts = []; + this._preventRetries = []; + this._thumbnailUrls = []; + + this._netUploadedOrQueued = 0; + this._netUploaded = 0; + this._uploadData = this._createUploadDataTracker(); + + this._initFormSupportAndParams(); + + this._customHeadersStore = this._createStore(this._options.request.customHeaders); + this._deleteFileCustomHeadersStore = this._createStore(this._options.deleteFile.customHeaders); + + this._deleteFileParamsStore = this._createStore(this._options.deleteFile.params); + + this._endpointStore = this._createStore(this._options.request.endpoint); + this._deleteFileEndpointStore = this._createStore(this._options.deleteFile.endpoint); + + this._handler = this._createUploadHandler(); + + this._deleteHandler = qq.DeleteFileAjaxRequester && this._createDeleteHandler(); + + if (this._options.button) { + this._defaultButtonId = this._createUploadButton({element: this._options.button}).getButtonId(); + } + + this._generateExtraButtonSpecs(); + + this._handleCameraAccess(); + + if (this._options.paste.targetElement) { + if (qq.PasteSupport) { + this._pasteHandler = this._createPasteHandler(); + } + else { + this.log("Paste support module not found", "error"); + } + } + + this._preventLeaveInProgress(); + + this._imageGenerator = qq.ImageGenerator && new qq.ImageGenerator(qq.bind(this.log, this)); + this._refreshSessionData(); + + this._succeededSinceLastAllComplete = []; + this._failedSinceLastAllComplete = []; + + this._scaler = (qq.Scaler && new qq.Scaler(this._options.scaling, qq.bind(this.log, this))) || {}; + if (this._scaler.enabled) { + this._customNewFileHandler = qq.bind(this._scaler.handleNewFile, this._scaler); + } + + if (qq.TotalProgress && qq.supportedFeatures.progressBar) { + this._totalProgress = new qq.TotalProgress( + qq.bind(this._onTotalProgress, this), + + function(id) { + var entry = self._uploadData.retrieve({id: id}); + return (entry && entry.size) || 0; + } + ); + } + + this._currentItemLimit = this._options.validation.itemLimit; + }; + + // Define the private & public API methods. + qq.FineUploaderBasic.prototype = qq.basePublicApi; + qq.extend(qq.FineUploaderBasic.prototype, qq.basePrivateApi); +}()); + +/*globals qq, XDomainRequest*/ +/** Generic class for sending non-upload ajax requests and handling the associated responses **/ +qq.AjaxRequester = function(o) { + "use strict"; + + var log, shouldParamsBeInQueryString, + queue = [], + requestData = {}, + options = { + acceptHeader: null, + validMethods: ["PATCH", "POST", "PUT"], + method: "POST", + contentType: "application/x-www-form-urlencoded", + maxConnections: 3, + customHeaders: {}, + endpointStore: {}, + paramsStore: {}, + mandatedParams: {}, + allowXRequestedWithAndCacheControl: true, + successfulResponseCodes: { + DELETE: [200, 202, 204], + PATCH: [200, 201, 202, 203, 204], + POST: [200, 201, 202, 203, 204], + PUT: [200, 201, 202, 203, 204], + GET: [200] + }, + cors: { + expected: false, + sendCredentials: false + }, + log: function(str, level) {}, + onSend: function(id) {}, + onComplete: function(id, xhrOrXdr, isError) {}, + onProgress: null + }; + + qq.extend(options, o); + log = options.log; + + if (qq.indexOf(options.validMethods, options.method) < 0) { + throw new Error("'" + options.method + "' is not a supported method for this type of request!"); + } + + // [Simple methods](http://www.w3.org/TR/cors/#simple-method) + // are defined by the W3C in the CORS spec as a list of methods that, in part, + // make a CORS request eligible to be exempt from preflighting. + function isSimpleMethod() { + return qq.indexOf(["GET", "POST", "HEAD"], options.method) >= 0; + } + + // [Simple headers](http://www.w3.org/TR/cors/#simple-header) + // are defined by the W3C in the CORS spec as a list of headers that, in part, + // make a CORS request eligible to be exempt from preflighting. + function containsNonSimpleHeaders(headers) { + var containsNonSimple = false; + + qq.each(containsNonSimple, function(idx, header) { + if (qq.indexOf(["Accept", "Accept-Language", "Content-Language", "Content-Type"], header) < 0) { + containsNonSimple = true; + return false; + } + }); + + return containsNonSimple; + } + + function isXdr(xhr) { + //The `withCredentials` test is a commonly accepted way to determine if XHR supports CORS. + return options.cors.expected && xhr.withCredentials === undefined; + } + + // Returns either a new `XMLHttpRequest` or `XDomainRequest` instance. + function getCorsAjaxTransport() { + var xhrOrXdr; + + if (window.XMLHttpRequest || window.ActiveXObject) { + xhrOrXdr = qq.createXhrInstance(); + + if (xhrOrXdr.withCredentials === undefined) { + xhrOrXdr = new XDomainRequest(); + // Workaround for XDR bug in IE9 - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + xhrOrXdr.onload = function() {}; + xhrOrXdr.onerror = function() {}; + xhrOrXdr.ontimeout = function() {}; + xhrOrXdr.onprogress = function() {}; + } + } + + return xhrOrXdr; + } + + // Returns either a new XHR/XDR instance, or an existing one for the associated `File` or `Blob`. + function getXhrOrXdr(id, suppliedXhr) { + var xhrOrXdr = requestData[id].xhr; + + if (!xhrOrXdr) { + if (suppliedXhr) { + xhrOrXdr = suppliedXhr; + } + else { + if (options.cors.expected) { + xhrOrXdr = getCorsAjaxTransport(); + } + else { + xhrOrXdr = qq.createXhrInstance(); + } + } + + requestData[id].xhr = xhrOrXdr; + } + + return xhrOrXdr; + } + + // Removes element from queue, sends next request + function dequeue(id) { + var i = qq.indexOf(queue, id), + max = options.maxConnections, + nextId; + + delete requestData[id]; + queue.splice(i, 1); + + if (queue.length >= max && i < max) { + nextId = queue[max - 1]; + sendRequest(nextId); + } + } + + function onComplete(id, xdrError) { + var xhr = getXhrOrXdr(id), + method = options.method, + isError = xdrError === true; + + dequeue(id); + + if (isError) { + log(method + " request for " + id + " has failed", "error"); + } + else if (!isXdr(xhr) && !isResponseSuccessful(xhr.status)) { + isError = true; + log(method + " request for " + id + " has failed - response code " + xhr.status, "error"); + } + + options.onComplete(id, xhr, isError); + } + + function getParams(id) { + var onDemandParams = requestData[id].additionalParams, + mandatedParams = options.mandatedParams, + params; + + if (options.paramsStore.get) { + params = options.paramsStore.get(id); + } + + if (onDemandParams) { + qq.each(onDemandParams, function(name, val) { + params = params || {}; + params[name] = val; + }); + } + + if (mandatedParams) { + qq.each(mandatedParams, function(name, val) { + params = params || {}; + params[name] = val; + }); + } + + return params; + } + + function sendRequest(id, optXhr) { + var xhr = getXhrOrXdr(id, optXhr), + method = options.method, + params = getParams(id), + payload = requestData[id].payload, + url; + + options.onSend(id); + + url = createUrl(id, params, requestData[id].additionalQueryParams); + + // XDR and XHR status detection APIs differ a bit. + if (isXdr(xhr)) { + xhr.onload = getXdrLoadHandler(id); + xhr.onerror = getXdrErrorHandler(id); + } + else { + xhr.onreadystatechange = getXhrReadyStateChangeHandler(id); + } + + registerForUploadProgress(id); + + // The last parameter is assumed to be ignored if we are actually using `XDomainRequest`. + xhr.open(method, url, true); + + // Instruct the transport to send cookies along with the CORS request, + // unless we are using `XDomainRequest`, which is not capable of this. + if (options.cors.expected && options.cors.sendCredentials && !isXdr(xhr)) { + xhr.withCredentials = true; + } + + setHeaders(id); + + log("Sending " + method + " request for " + id); + + if (payload) { + xhr.send(payload); + } + else if (shouldParamsBeInQueryString || !params) { + xhr.send(); + } + else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/x-www-form-urlencoded") >= 0) { + xhr.send(qq.obj2url(params, "")); + } + else if (params && options.contentType && options.contentType.toLowerCase().indexOf("application/json") >= 0) { + xhr.send(JSON.stringify(params)); + } + else { + xhr.send(params); + } + + return xhr; + } + + function createUrl(id, params, additionalQueryParams) { + var endpoint = options.endpointStore.get(id), + addToPath = requestData[id].addToPath; + + /*jshint -W116,-W041 */ + if (addToPath != undefined) { + endpoint += "/" + addToPath; + } + + if (shouldParamsBeInQueryString && params) { + endpoint = qq.obj2url(params, endpoint); + } + + if (additionalQueryParams) { + endpoint = qq.obj2url(additionalQueryParams, endpoint); + } + + return endpoint; + } + + // Invoked by the UA to indicate a number of possible states that describe + // a live `XMLHttpRequest` transport. + function getXhrReadyStateChangeHandler(id) { + return function() { + if (getXhrOrXdr(id).readyState === 4) { + onComplete(id); + } + }; + } + + function registerForUploadProgress(id) { + var onProgress = options.onProgress; + + if (onProgress) { + getXhrOrXdr(id).upload.onprogress = function(e) { + if (e.lengthComputable) { + onProgress(id, e.loaded, e.total); + } + }; + } + } + + // This will be called by IE to indicate **success** for an associated + // `XDomainRequest` transported request. + function getXdrLoadHandler(id) { + return function() { + onComplete(id); + }; + } + + // This will be called by IE to indicate **failure** for an associated + // `XDomainRequest` transported request. + function getXdrErrorHandler(id) { + return function() { + onComplete(id, true); + }; + } + + function setHeaders(id) { + var xhr = getXhrOrXdr(id), + customHeaders = options.customHeaders, + onDemandHeaders = requestData[id].additionalHeaders || {}, + method = options.method, + allHeaders = {}; + + // If XDomainRequest is being used, we can't set headers, so just ignore this block. + if (!isXdr(xhr)) { + options.acceptHeader && xhr.setRequestHeader("Accept", options.acceptHeader); + + // Only attempt to add X-Requested-With & Cache-Control if permitted + if (options.allowXRequestedWithAndCacheControl) { + // Do not add X-Requested-With & Cache-Control if this is a cross-origin request + // OR the cross-origin request contains a non-simple method or header. + // This is done to ensure a preflight is not triggered exclusively based on the + // addition of these 2 non-simple headers. + if (!options.cors.expected || (!isSimpleMethod() || containsNonSimpleHeaders(customHeaders))) { + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + xhr.setRequestHeader("Cache-Control", "no-cache"); + } + } + + if (options.contentType && (method === "POST" || method === "PUT")) { + xhr.setRequestHeader("Content-Type", options.contentType); + } + + qq.extend(allHeaders, qq.isFunction(customHeaders) ? customHeaders(id) : customHeaders); + qq.extend(allHeaders, onDemandHeaders); + + qq.each(allHeaders, function(name, val) { + xhr.setRequestHeader(name, val); + }); + } + } + + function isResponseSuccessful(responseCode) { + return qq.indexOf(options.successfulResponseCodes[options.method], responseCode) >= 0; + } + + function prepareToSend(id, optXhr, addToPath, additionalParams, additionalQueryParams, additionalHeaders, payload) { + requestData[id] = { + addToPath: addToPath, + additionalParams: additionalParams, + additionalQueryParams: additionalQueryParams, + additionalHeaders: additionalHeaders, + payload: payload + }; + + var len = queue.push(id); + + // if too many active connections, wait... + if (len <= options.maxConnections) { + return sendRequest(id, optXhr); + } + } + + shouldParamsBeInQueryString = options.method === "GET" || options.method === "DELETE"; + + qq.extend(this, { + // Start the process of sending the request. The ID refers to the file associated with the request. + initTransport: function(id) { + var path, params, headers, payload, cacheBuster, additionalQueryParams; + + return { + // Optionally specify the end of the endpoint path for the request. + withPath: function(appendToPath) { + path = appendToPath; + return this; + }, + + // Optionally specify additional parameters to send along with the request. + // These will be added to the query string for GET/DELETE requests or the payload + // for POST/PUT requests. The Content-Type of the request will be used to determine + // how these parameters should be formatted as well. + withParams: function(additionalParams) { + params = additionalParams; + return this; + }, + + withQueryParams: function(_additionalQueryParams_) { + additionalQueryParams = _additionalQueryParams_; + return this; + }, + + // Optionally specify additional headers to send along with the request. + withHeaders: function(additionalHeaders) { + headers = additionalHeaders; + return this; + }, + + // Optionally specify a payload/body for the request. + withPayload: function(thePayload) { + payload = thePayload; + return this; + }, + + // Appends a cache buster (timestamp) to the request URL as a query parameter (only if GET or DELETE) + withCacheBuster: function() { + cacheBuster = true; + return this; + }, + + // Send the constructed request. + send: function(optXhr) { + if (cacheBuster && qq.indexOf(["GET", "DELETE"], options.method) >= 0) { + params.qqtimestamp = new Date().getTime(); + } + + return prepareToSend(id, optXhr, path, params, additionalQueryParams, headers, payload); + } + }; + }, + + canceled: function(id) { + dequeue(id); + } + }); +}; + +/* globals qq */ +/** + * Common upload handler functions. + * + * @constructor + */ +qq.UploadHandler = function(spec) { + "use strict"; + + var proxy = spec.proxy, + fileState = {}, + onCancel = proxy.onCancel, + getName = proxy.getName; + + qq.extend(this, { + add: function(id, fileItem) { + fileState[id] = fileItem; + fileState[id].temp = {}; + }, + + cancel: function(id) { + var self = this, + cancelFinalizationEffort = new qq.Promise(), + onCancelRetVal = onCancel(id, getName(id), cancelFinalizationEffort); + + onCancelRetVal.then(function() { + if (self.isValid(id)) { + fileState[id].canceled = true; + self.expunge(id); + } + cancelFinalizationEffort.success(); + }); + }, + + expunge: function(id) { + delete fileState[id]; + }, + + getThirdPartyFileId: function(id) { + return fileState[id].key; + }, + + isValid: function(id) { + return fileState[id] !== undefined; + }, + + reset: function() { + fileState = {}; + }, + + _getFileState: function(id) { + return fileState[id]; + }, + + _setThirdPartyFileId: function(id, thirdPartyFileId) { + fileState[id].key = thirdPartyFileId; + }, + + _wasCanceled: function(id) { + return !!fileState[id].canceled; + } + }); +}; + +/*globals qq*/ +/** + * Base upload handler module. Controls more specific handlers. + * + * @param o Options. Passed along to the specific handler submodule as well. + * @param namespace [optional] Namespace for the specific handler. + */ +qq.UploadHandlerController = function(o, namespace) { + "use strict"; + + var controller = this, + chunkingPossible = false, + concurrentChunkingPossible = false, + chunking, preventRetryResponse, log, handler, + + options = { + paramsStore: {}, + maxConnections: 3, // maximum number of concurrent uploads + chunking: { + enabled: false, + multiple: { + enabled: false + } + }, + log: function(str, level) {}, + onProgress: function(id, fileName, loaded, total) {}, + onComplete: function(id, fileName, response, xhr) {}, + onCancel: function(id, fileName) {}, + onUploadPrep: function(id) {}, // Called if non-trivial operations will be performed before onUpload + onUpload: function(id, fileName) {}, + onUploadChunk: function(id, fileName, chunkData) {}, + onUploadChunkSuccess: function(id, chunkData, response, xhr) {}, + onAutoRetry: function(id, fileName, response, xhr) {}, + onResume: function(id, fileName, chunkData) {}, + onUuidChanged: function(id, newUuid) {}, + getName: function(id) {}, + setSize: function(id, newSize) {}, + isQueued: function(id) {}, + getIdsInProxyGroup: function(id) {}, + getIdsInBatch: function(id) {} + }, + + chunked = { + // Called when each chunk has uploaded successfully + done: function(id, chunkIdx, response, xhr) { + var chunkData = handler._getChunkData(id, chunkIdx); + + handler._getFileState(id).attemptingResume = false; + + delete handler._getFileState(id).temp.chunkProgress[chunkIdx]; + handler._getFileState(id).loaded += chunkData.size; + + options.onUploadChunkSuccess(id, handler._getChunkDataForCallback(chunkData), response, xhr); + }, + + // Called when all chunks have been successfully uploaded and we want to ask the handler to perform any + // logic associated with closing out the file, such as combining the chunks. + finalize: function(id) { + var size = options.getSize(id), + name = options.getName(id); + + log("All chunks have been uploaded for " + id + " - finalizing...."); + handler.finalizeChunks(id).then( + function(response, xhr) { + log("Finalize successful for " + id); + + var normaizedResponse = upload.normalizeResponse(response, true); + + options.onProgress(id, name, size, size); + handler._maybeDeletePersistedChunkData(id); + upload.cleanup(id, normaizedResponse, xhr); + }, + function(response, xhr) { + var normaizedResponse = upload.normalizeResponse(response, false); + + log("Problem finalizing chunks for file ID " + id + " - " + normaizedResponse.error, "error"); + + if (normaizedResponse.reset) { + chunked.reset(id); + } + + if (!options.onAutoRetry(id, name, normaizedResponse, xhr)) { + upload.cleanup(id, normaizedResponse, xhr); + } + } + ); + }, + + hasMoreParts: function(id) { + return !!handler._getFileState(id).chunking.remaining.length; + }, + + nextPart: function(id) { + var nextIdx = handler._getFileState(id).chunking.remaining.shift(); + + if (nextIdx >= handler._getTotalChunks(id)) { + nextIdx = null; + } + + return nextIdx; + }, + + reset: function(id) { + log("Server or callback has ordered chunking effort to be restarted on next attempt for item ID " + id, "error"); + + handler._maybeDeletePersistedChunkData(id); + handler.reevaluateChunking(id); + handler._getFileState(id).loaded = 0; + }, + + sendNext: function(id) { + var size = options.getSize(id), + name = options.getName(id), + chunkIdx = chunked.nextPart(id), + chunkData = handler._getChunkData(id, chunkIdx), + resuming = handler._getFileState(id).attemptingResume, + inProgressChunks = handler._getFileState(id).chunking.inProgress || []; + + if (handler._getFileState(id).loaded == null) { + handler._getFileState(id).loaded = 0; + } + + // Don't follow-through with the resume attempt if the integrator returns false from onResume + if (resuming && options.onResume(id, name, chunkData) === false) { + chunked.reset(id); + chunkIdx = chunked.nextPart(id); + chunkData = handler._getChunkData(id, chunkIdx); + resuming = false; + } + + // If all chunks have already uploaded successfully, we must be re-attempting the finalize step. + if (chunkIdx == null && inProgressChunks.length === 0) { + chunked.finalize(id); + } + + // Send the next chunk + else { + log(qq.format("Sending chunked upload request for item {}.{}, bytes {}-{} of {}.", id, chunkIdx, chunkData.start + 1, chunkData.end, size)); + options.onUploadChunk(id, name, handler._getChunkDataForCallback(chunkData)); + inProgressChunks.push(chunkIdx); + handler._getFileState(id).chunking.inProgress = inProgressChunks; + + if (concurrentChunkingPossible) { + connectionManager.open(id, chunkIdx); + } + + if (concurrentChunkingPossible && connectionManager.available() && handler._getFileState(id).chunking.remaining.length) { + chunked.sendNext(id); + } + + handler.uploadChunk(id, chunkIdx, resuming).then( + // upload chunk success + function success(response, xhr) { + log("Chunked upload request succeeded for " + id + ", chunk " + chunkIdx); + + handler.clearCachedChunk(id, chunkIdx); + + var inProgressChunks = handler._getFileState(id).chunking.inProgress || [], + responseToReport = upload.normalizeResponse(response, true), + inProgressChunkIdx = qq.indexOf(inProgressChunks, chunkIdx); + + log(qq.format("Chunk {} for file {} uploaded successfully.", chunkIdx, id)); + + chunked.done(id, chunkIdx, responseToReport, xhr); + + if (inProgressChunkIdx >= 0) { + inProgressChunks.splice(inProgressChunkIdx, 1); + } + + handler._maybePersistChunkedState(id); + + if (!chunked.hasMoreParts(id) && inProgressChunks.length === 0) { + chunked.finalize(id); + } + else if (chunked.hasMoreParts(id)) { + chunked.sendNext(id); + } + else { + log(qq.format("File ID {} has no more chunks to send and these chunk indexes are still marked as in-progress: {}", id, JSON.stringify(inProgressChunks))); + } + }, + + // upload chunk failure + function failure(response, xhr) { + log("Chunked upload request failed for " + id + ", chunk " + chunkIdx); + + handler.clearCachedChunk(id, chunkIdx); + + var responseToReport = upload.normalizeResponse(response, false), + inProgressIdx; + + if (responseToReport.reset) { + chunked.reset(id); + } + else { + inProgressIdx = qq.indexOf(handler._getFileState(id).chunking.inProgress, chunkIdx); + if (inProgressIdx >= 0) { + handler._getFileState(id).chunking.inProgress.splice(inProgressIdx, 1); + handler._getFileState(id).chunking.remaining.unshift(chunkIdx); + } + } + + // We may have aborted all other in-progress chunks for this file due to a failure. + // If so, ignore the failures associated with those aborts. + if (!handler._getFileState(id).temp.ignoreFailure) { + // If this chunk has failed, we want to ignore all other failures of currently in-progress + // chunks since they will be explicitly aborted + if (concurrentChunkingPossible) { + handler._getFileState(id).temp.ignoreFailure = true; + + log(qq.format("Going to attempt to abort these chunks: {}. These are currently in-progress: {}.", JSON.stringify(Object.keys(handler._getXhrs(id))), JSON.stringify(handler._getFileState(id).chunking.inProgress))); + qq.each(handler._getXhrs(id), function(ckid, ckXhr) { + log(qq.format("Attempting to abort file {}.{}. XHR readyState {}. ", id, ckid, ckXhr.readyState)); + ckXhr.abort(); + // Flag the transport, in case we are waiting for some other async operation + // to complete before attempting to upload the chunk + ckXhr._cancelled = true; + }); + + // We must indicate that all aborted chunks are no longer in progress + handler.moveInProgressToRemaining(id); + + // Free up any connections used by these chunks, but don't allow any + // other files to take up the connections (until we have exhausted all auto-retries) + connectionManager.free(id, true); + } + + if (!options.onAutoRetry(id, name, responseToReport, xhr)) { + // If one chunk fails, abort all of the others to avoid odd race conditions that occur + // if a chunk succeeds immediately after one fails before we have determined if the upload + // is a failure or not. + upload.cleanup(id, responseToReport, xhr); + } + } + } + ) + .done(function() { + handler.clearXhr(id, chunkIdx); + }) ; + } + } + }, + + connectionManager = { + _open: [], + _openChunks: {}, + _waiting: [], + + available: function() { + var max = options.maxConnections, + openChunkEntriesCount = 0, + openChunksCount = 0; + + qq.each(connectionManager._openChunks, function(fileId, openChunkIndexes) { + openChunkEntriesCount++; + openChunksCount += openChunkIndexes.length; + }); + + return max - (connectionManager._open.length - openChunkEntriesCount + openChunksCount); + }, + + /** + * Removes element from queue, starts upload of next + */ + free: function(id, dontAllowNext) { + var allowNext = !dontAllowNext, + waitingIndex = qq.indexOf(connectionManager._waiting, id), + connectionsIndex = qq.indexOf(connectionManager._open, id), + nextId; + + delete connectionManager._openChunks[id]; + + if (upload.getProxyOrBlob(id) instanceof qq.BlobProxy) { + log("Generated blob upload has ended for " + id + ", disposing generated blob."); + delete handler._getFileState(id).file; + } + + // If this file was not consuming a connection, it was just waiting, so remove it from the waiting array + if (waitingIndex >= 0) { + connectionManager._waiting.splice(waitingIndex, 1); + } + // If this file was consuming a connection, allow the next file to be uploaded + else if (allowNext && connectionsIndex >= 0) { + connectionManager._open.splice(connectionsIndex, 1); + + nextId = connectionManager._waiting.shift(); + if (nextId >= 0) { + connectionManager._open.push(nextId); + upload.start(nextId); + } + } + }, + + getWaitingOrConnected: function() { + var waitingOrConnected = []; + + // Chunked files may have multiple connections open per chunk (if concurrent chunking is enabled) + // We need to grab the file ID of any file that has at least one chunk consuming a connection. + qq.each(connectionManager._openChunks, function(fileId, chunks) { + if (chunks && chunks.length) { + waitingOrConnected.push(parseInt(fileId)); + } + }); + + // For non-chunked files, only one connection will be consumed per file. + // This is where we aggregate those file IDs. + qq.each(connectionManager._open, function(idx, fileId) { + if (!connectionManager._openChunks[fileId]) { + waitingOrConnected.push(parseInt(fileId)); + } + }); + + // There may be files waiting for a connection. + waitingOrConnected = waitingOrConnected.concat(connectionManager._waiting); + + return waitingOrConnected; + }, + + isUsingConnection: function(id) { + return qq.indexOf(connectionManager._open, id) >= 0; + }, + + open: function(id, chunkIdx) { + if (chunkIdx == null) { + connectionManager._waiting.push(id); + } + + if (connectionManager.available()) { + if (chunkIdx == null) { + connectionManager._waiting.pop(); + connectionManager._open.push(id); + } + else { + (function() { + var openChunksEntry = connectionManager._openChunks[id] || []; + openChunksEntry.push(chunkIdx); + connectionManager._openChunks[id] = openChunksEntry; + }()); + } + + return true; + } + + return false; + }, + + reset: function() { + connectionManager._waiting = []; + connectionManager._open = []; + } + }, + + simple = { + send: function(id, name) { + handler._getFileState(id).loaded = 0; + + log("Sending simple upload request for " + id); + handler.uploadFile(id).then( + function(response, optXhr) { + log("Simple upload request succeeded for " + id); + + var responseToReport = upload.normalizeResponse(response, true), + size = options.getSize(id); + + options.onProgress(id, name, size, size); + upload.maybeNewUuid(id, responseToReport); + upload.cleanup(id, responseToReport, optXhr); + }, + + function(response, optXhr) { + log("Simple upload request failed for " + id); + + var responseToReport = upload.normalizeResponse(response, false); + + if (!options.onAutoRetry(id, name, responseToReport, optXhr)) { + upload.cleanup(id, responseToReport, optXhr); + } + } + ); + } + }, + + upload = { + cancel: function(id) { + log("Cancelling " + id); + options.paramsStore.remove(id); + connectionManager.free(id); + }, + + cleanup: function(id, response, optXhr) { + var name = options.getName(id); + + options.onComplete(id, name, response, optXhr); + + if (handler._getFileState(id)) { + handler._clearXhrs && handler._clearXhrs(id); + } + + connectionManager.free(id); + }, + + // Returns a qq.BlobProxy, or an actual File/Blob if no proxy is involved, or undefined + // if none of these are available for the ID + getProxyOrBlob: function(id) { + return (handler.getProxy && handler.getProxy(id)) || + (handler.getFile && handler.getFile(id)); + }, + + initHandler: function() { + var handlerType = namespace ? qq[namespace] : qq.traditional, + handlerModuleSubtype = qq.supportedFeatures.ajaxUploading ? "Xhr" : "Form"; + + handler = new handlerType[handlerModuleSubtype + "UploadHandler"]( + options, + { + getDataByUuid: options.getDataByUuid, + getName: options.getName, + getSize: options.getSize, + getUuid: options.getUuid, + log: log, + onCancel: options.onCancel, + onProgress: options.onProgress, + onUuidChanged: options.onUuidChanged + } + ); + + if (handler._removeExpiredChunkingRecords) { + handler._removeExpiredChunkingRecords(); + } + }, + + isDeferredEligibleForUpload: function(id) { + return options.isQueued(id); + }, + + // For Blobs that are part of a group of generated images, along with a reference image, + // this will ensure the blobs in the group are uploaded in the order they were triggered, + // even if some async processing must be completed on one or more Blobs first. + maybeDefer: function(id, blob) { + // If we don't have a file/blob yet & no file/blob exists for this item, request it, + // and then submit the upload to the specific handler once the blob is available. + // ASSUMPTION: This condition will only ever be true if XHR uploading is supported. + if (blob && !handler.getFile(id) && blob instanceof qq.BlobProxy) { + + // Blob creation may take some time, so the caller may want to update the + // UI to indicate that an operation is in progress, even before the actual + // upload begins and an onUpload callback is invoked. + options.onUploadPrep(id); + + log("Attempting to generate a blob on-demand for " + id); + blob.create().then(function(generatedBlob) { + log("Generated an on-demand blob for " + id); + + // Update record associated with this file by providing the generated Blob + handler.updateBlob(id, generatedBlob); + + // Propagate the size for this generated Blob + options.setSize(id, generatedBlob.size); + + // Order handler to recalculate chunking possibility, if applicable + handler.reevaluateChunking(id); + + upload.maybeSendDeferredFiles(id); + }, + + // Blob could not be generated. Fail the upload & attempt to prevent retries. Also bubble error message. + function(errorMessage) { + var errorResponse = {}; + + if (errorMessage) { + errorResponse.error = errorMessage; + } + + log(qq.format("Failed to generate blob for ID {}. Error message: {}.", id, errorMessage), "error"); + + options.onComplete(id, options.getName(id), qq.extend(errorResponse, preventRetryResponse), null); + upload.maybeSendDeferredFiles(id); + connectionManager.free(id); + }); + } + else { + return upload.maybeSendDeferredFiles(id); + } + + return false; + }, + + // Upload any grouped blobs, in the proper order, that are ready to be uploaded + maybeSendDeferredFiles: function(id) { + var idsInGroup = options.getIdsInProxyGroup(id), + uploadedThisId = false; + + if (idsInGroup && idsInGroup.length) { + log("Maybe ready to upload proxy group file " + id); + + qq.each(idsInGroup, function(idx, idInGroup) { + if (upload.isDeferredEligibleForUpload(idInGroup) && !!handler.getFile(idInGroup)) { + uploadedThisId = idInGroup === id; + upload.now(idInGroup); + } + else if (upload.isDeferredEligibleForUpload(idInGroup)) { + return false; + } + }); + } + else { + uploadedThisId = true; + upload.now(id); + } + + return uploadedThisId; + }, + + maybeNewUuid: function(id, response) { + if (response.newUuid !== undefined) { + options.onUuidChanged(id, response.newUuid); + } + }, + + // The response coming from handler implementations may be in various formats. + // Instead of hoping a promise nested 5 levels deep will always return an object + // as its first param, let's just normalize the response here. + normalizeResponse: function(originalResponse, successful) { + var response = originalResponse; + + // The passed "response" param may not be a response at all. + // It could be a string, detailing the error, for example. + if (!qq.isObject(originalResponse)) { + response = {}; + + if (qq.isString(originalResponse) && !successful) { + response.error = originalResponse; + } + } + + response.success = successful; + + return response; + }, + + now: function(id) { + var name = options.getName(id); + + if (!controller.isValid(id)) { + throw new qq.Error(id + " is not a valid file ID to upload!"); + } + + options.onUpload(id, name); + + if (chunkingPossible && handler._shouldChunkThisFile(id)) { + chunked.sendNext(id); + } + else { + simple.send(id, name); + } + }, + + start: function(id) { + var blobToUpload = upload.getProxyOrBlob(id); + + if (blobToUpload) { + return upload.maybeDefer(id, blobToUpload); + } + else { + upload.now(id); + return true; + } + } + }; + + qq.extend(this, { + /** + * Adds file or file input to the queue + **/ + add: function(id, file) { + handler.add.apply(this, arguments); + }, + + /** + * Sends the file identified by id + */ + upload: function(id) { + if (connectionManager.open(id)) { + return upload.start(id); + } + return false; + }, + + retry: function(id) { + // On retry, if concurrent chunking has been enabled, we may have aborted all other in-progress chunks + // for a file when encountering a failed chunk upload. We then signaled the controller to ignore + // all failures associated with these aborts. We are now retrying, so we don't want to ignore + // any more failures at this point. + if (concurrentChunkingPossible) { + handler._getFileState(id).temp.ignoreFailure = false; + } + + // If we are attempting to retry a file that is already consuming a connection, this is likely an auto-retry. + // Just go ahead and ask the handler to upload again. + if (connectionManager.isUsingConnection(id)) { + return upload.start(id); + } + + // If we are attempting to retry a file that is not currently consuming a connection, + // this is likely a manual retry attempt. We will need to ensure a connection is available + // before the retry commences. + else { + return controller.upload(id); + } + }, + + /** + * Cancels file upload by id + */ + cancel: function(id) { + var cancelRetVal = handler.cancel(id); + + if (qq.isGenericPromise(cancelRetVal)) { + cancelRetVal.then(function() { + upload.cancel(id); + }); + } + else if (cancelRetVal !== false) { + upload.cancel(id); + } + }, + + /** + * Cancels all queued or in-progress uploads + */ + cancelAll: function() { + var waitingOrConnected = connectionManager.getWaitingOrConnected(), + i; + + // ensure files are cancelled in reverse order which they were added + // to avoid a flash of time where a queued file begins to upload before it is canceled + if (waitingOrConnected.length) { + for (i = waitingOrConnected.length - 1; i >= 0; i--) { + controller.cancel(waitingOrConnected[i]); + } + } + + connectionManager.reset(); + }, + + // Returns a File, Blob, or the Blob/File for the reference/parent file if the targeted blob is a proxy. + // Undefined if no file record is available. + getFile: function(id) { + if (handler.getProxy && handler.getProxy(id)) { + return handler.getProxy(id).referenceBlob; + } + + return handler.getFile && handler.getFile(id); + }, + + // Returns true if the Blob associated with the ID is related to a proxy s + isProxied: function(id) { + return !!(handler.getProxy && handler.getProxy(id)); + }, + + getInput: function(id) { + if (handler.getInput) { + return handler.getInput(id); + } + }, + + reset: function() { + log("Resetting upload handler"); + controller.cancelAll(); + connectionManager.reset(); + handler.reset(); + }, + + expunge: function(id) { + if (controller.isValid(id)) { + return handler.expunge(id); + } + }, + + /** + * Determine if the file exists. + */ + isValid: function(id) { + return handler.isValid(id); + }, + + getResumableFilesData: function() { + if (handler.getResumableFilesData) { + return handler.getResumableFilesData(); + } + return []; + }, + + /** + * This may or may not be implemented, depending on the handler. For handlers where a third-party ID is + * available (such as the "key" for Amazon S3), this will return that value. Otherwise, the return value + * will be undefined. + * + * @param id Internal file ID + * @returns {*} Some identifier used by a 3rd-party service involved in the upload process + */ + getThirdPartyFileId: function(id) { + if (controller.isValid(id)) { + return handler.getThirdPartyFileId(id); + } + }, + + /** + * Attempts to pause the associated upload if the specific handler supports this and the file is "valid". + * @param id ID of the upload/file to pause + * @returns {boolean} true if the upload was paused + */ + pause: function(id) { + if (controller.isResumable(id) && handler.pause && controller.isValid(id) && handler.pause(id)) { + connectionManager.free(id); + handler.moveInProgressToRemaining(id); + return true; + } + return false; + }, + + // True if the file is eligible for pause/resume. + isResumable: function(id) { + return !!handler.isResumable && handler.isResumable(id); + } + }); + + qq.extend(options, o); + log = options.log; + chunkingPossible = options.chunking.enabled && qq.supportedFeatures.chunking; + concurrentChunkingPossible = chunkingPossible && options.chunking.concurrent.enabled; + + preventRetryResponse = (function() { + var response = {}; + + response[options.preventRetryParam] = true; + + return response; + }()); + + upload.initHandler(); +}; + +/* globals qq */ +/** + * Common APIs exposed to creators of upload via form/iframe handlers. This is reused and possibly overridden + * in some cases by specific form upload handlers. + * + * @constructor + */ +qq.FormUploadHandler = function(spec) { + "use strict"; + + var options = spec.options, + handler = this, + proxy = spec.proxy, + formHandlerInstanceId = qq.getUniqueId(), + onloadCallbacks = {}, + detachLoadEvents = {}, + postMessageCallbackTimers = {}, + isCors = options.isCors, + inputName = options.inputName, + getUuid = proxy.getUuid, + log = proxy.log, + corsMessageReceiver = new qq.WindowReceiveMessage({log: log}); + + /** + * Remove any trace of the file from the handler. + * + * @param id ID of the associated file + */ + function expungeFile(id) { + delete detachLoadEvents[id]; + + // If we are dealing with CORS, we might still be waiting for a response from a loaded iframe. + // In that case, terminate the timer waiting for a message from the loaded iframe + // and stop listening for any more messages coming from this iframe. + if (isCors) { + clearTimeout(postMessageCallbackTimers[id]); + delete postMessageCallbackTimers[id]; + corsMessageReceiver.stopReceivingMessages(id); + } + + var iframe = document.getElementById(handler._getIframeName(id)); + if (iframe) { + // To cancel request set src to something else. We use src="javascript:false;" + // because it doesn't trigger ie6 prompt on https + /* jshint scripturl:true */ + iframe.setAttribute("src", "javascript:false;"); + + qq(iframe).remove(); + } + } + + /** + * @param iframeName `document`-unique Name of the associated iframe + * @returns {*} ID of the associated file + */ + function getFileIdForIframeName(iframeName) { + return iframeName.split("_")[0]; + } + + /** + * Generates an iframe to be used as a target for upload-related form submits. This also adds the iframe + * to the current `document`. Note that the iframe is hidden from view. + * + * @param name Name of the iframe. + * @returns {HTMLIFrameElement} The created iframe + */ + function initIframeForUpload(name) { + var iframe = qq.toElement("