From 02497d8e981d16f20665651e084a4e3c9e4fc930 Mon Sep 17 00:00:00 2001 From: Spitzer Daniel Date: Mon, 1 Jan 2024 14:16:31 +0100 Subject: [PATCH 1/6] Mobile Integration,Pop Multiple Networks,DataTables responsible update,Migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mobile Integration: * in footer.php js eingefügt damit das mobile Menu funktioniert * in menu.php bzw. app.css neue Klasse eingefügt mobile-hide um in der mobilen Version Menupünkte zu verstecken Pop Multiple Networks * Pops können nun mehrere Netzgebiete haben * Netzgebiete und Pop ansicht angepasst * (Script muss ausgeführt werden um die PopNetwork Table vom Bestand zu befüllen) DataTables responsible update * Datatables update und responsible addon * Diverse Anpassungen für Responsible in: - Pops, Geräte Hersteller, Geräte Typen, Devices, Benutzer Migrations * PopNetwork * Poprackmodulepatch --- Layout/default/Device/Detail.php | 18 +- Layout/default/Device/Form.php | 3 + Layout/default/Device/Index.php | 25 +- Layout/default/Devicemanufactor/Index.php | 3 +- Layout/default/Devicetype/Index.php | 16 +- Layout/default/Network/Index.php | 12 +- Layout/default/Pop/Detail.php | 596 +- Layout/default/Pop/Form.php | 19 +- Layout/default/Pop/Index.php | 22 +- Layout/default/User/Index.php | 4 +- Layout/default/UserProfile/Form.php | 15 +- Layout/default/UserProfile/Index.php | 315 +- Layout/default/footer.php | 14 + Layout/default/header.php | 4 +- Layout/default/menu.php | 24 +- Layout/default/mfLogin/Index.php | 22 +- application/Network/Network.php | 2 +- application/Pop/PopController.php | 34 +- application/Pop/PopModel.php | 273 +- application/PopNetwork/PopNetwork.php | 44 + application/PopNetwork/PopNetworkModel.php | 154 + application/Poprack/PoprackModel.php | 22 + .../Poprackmodule/PoprackmoduleController.php | 31 +- .../Poprackmodulepatch/Poprackmodulepatch.php | 44 + .../PoprackmodulepatchModel.php | 108 + .../20231219144324_poprackmodulepatch.php | 41 + db/migrations/20240101121050_pop_network.php | 36 + public/assets/css/app.css | 11 +- public/assets/css/datatables-std.css | 105 +- public/datatables/datatables.css | 598 +- public/datatables/datatables.js | 116792 +++++++++------ public/datatables/datatables.min.css | 15 +- public/datatables/datatables.min.js | 196 +- public/datatables/json/german.json | 2 +- 34 files changed, 74059 insertions(+), 45561 deletions(-) create mode 100644 application/PopNetwork/PopNetwork.php create mode 100644 application/PopNetwork/PopNetworkModel.php create mode 100644 application/Poprackmodulepatch/Poprackmodulepatch.php create mode 100644 application/Poprackmodulepatch/PoprackmodulepatchModel.php create mode 100644 db/migrations/20231219144324_poprackmodulepatch.php create mode 100644 db/migrations/20240101121050_pop_network.php diff --git a/Layout/default/Device/Detail.php b/Layout/default/Device/Detail.php index 6bf9ed054..efaec921f 100644 --- a/Layout/default/Device/Detail.php +++ b/Layout/default/Device/Detail.php @@ -5,6 +5,7 @@
@@ -36,20 +35,21 @@ $pagination_entity_name = "Device";

Liste aller Devices

-
+
"> Neues Device anlegen + class="fas fa-plus"> Neues Device anlegen
- - - - - + + + + + @@ -112,8 +112,9 @@ $pagination_entity_name = "Device"; ?> - diff --git a/Layout/default/Devicemanufactor/Index.php b/Layout/default/Devicemanufactor/Index.php index f7a4f8e2f..a8fe21d29 100644 --- a/Layout/default/Devicemanufactor/Index.php +++ b/Layout/default/Devicemanufactor/Index.php @@ -31,7 +31,8 @@ $pagination_entity_name = "Devicemanufactor";
"> Neuen Gerätehersteller anlegen + class="fas fa-plus"> Neuen Gerätehersteller anlegen
diff --git a/Layout/default/Devicetype/Index.php b/Layout/default/Devicetype/Index.php index f29dfae9e..8e0be3aa5 100644 --- a/Layout/default/Devicetype/Index.php +++ b/Layout/default/Devicetype/Index.php @@ -4,7 +4,8 @@ $pagination_baseurl_params = ["filter" => $filter]; $pagination_entity_name = "Devicetype"; ?> - +
@@ -32,14 +33,11 @@ $pagination_entity_name = "Devicetype";
"> Neuen Gerätetyp anlegen + class="fas fa-plus"> Neuen Gerätetyp anlegen
- - - -
Device NameGeräte TypHerstellerPop/AdresseIP-AdresseDevice NameGeräte TypHerstellerPop/AdresseIP-Adresse Mac-Adresse Seriennummer Preis
- $device->id]) ?>">name ?> + + $device->id]) ?>">name ?> devicetype->name ?> devicetype->devicemanufactor->name ?>
@@ -98,15 +96,15 @@ $pagination_entity_name = "Devicetype"; - - + \ No newline at end of file diff --git a/Layout/default/Network/Index.php b/Layout/default/Network/Index.php index d78edf19a..70a22a633 100644 --- a/Layout/default/Network/Index.php +++ b/Layout/default/Network/Index.php @@ -106,13 +106,13 @@ pops as $pop): ?> - - - - + + + + diff --git a/Layout/default/Pop/Detail.php b/Layout/default/Pop/Detail.php index 270410f99..1793fd0e0 100644 --- a/Layout/default/Pop/Detail.php +++ b/Layout/default/Pop/Detail.php @@ -1,5 +1,5 @@ - +
@@ -67,9 +74,11 @@ if ($twofa == 0) { value="email ?>" disabled="disabled" />
-
+
- Um die Email Adresse zu ändern bitte 2FA auf SMS umschalten. +
Um die Email Adresse zu ändern + bitte 2FA auf SMS umschalten. +
@@ -84,7 +93,7 @@ if ($twofa == 0) { value="mobile ?>" disabled="disabled" />
-
+
Um die Mobilnummer zu ändern bitte 2FA auf Email umschalten.
diff --git a/Layout/default/UserProfile/Index.php b/Layout/default/UserProfile/Index.php index 4dc4156e3..7a69cfb52 100644 --- a/Layout/default/UserProfile/Index.php +++ b/Layout/default/UserProfile/Index.php @@ -1,29 +1,8 @@ - + -
@@ -31,151 +10,187 @@
-

Benutzerprofil

+

Gerätetyp

- -twofactor == 0) { - $twoFactorsym = ''; - $twoFactorbtn = ''; - $twoFactortype = '
-
'; - $twoFactorFormaction = "code2faaction"; -} else if ($userprofile->twofactor == 1) { - $twoFactorsym = ''; - $twoFactorbtn = ''; - $twoFactortype = ''; - $twoFactorFormaction = "change2faaction"; -} else if ($userprofile->twofactor == 2) { - $twoFactorsym = ''; - $twoFactorbtn = ''; - $twoFactortype = ''; - $twoFactorFormaction = "change2faaction"; -} -?> - -
-
-
-
-

Allgemeine Daten

-
-
Name
-
- name ?> -
+
+
+
+
+
+

Liste aller Gerätetypen

-
-
Email
-
- email ?> -
+ -
-
Mobiltelefon
-
- mobile ?> -
-
-
- -
-
+
+
-

Passwort ändern

-
"> -
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
- -
-

2FA

- -
"> -
- - -
- + + + +
name?>gps_lat?>, gps_long?>location)?>
pop->name?>pop->gps_lat?>, pop->gps_long?>pop->location)?> - $pop->id])?>"> - $pop->id])?>" class="text-danger" title="Löschen"> + $pop->pop->id])?>"> + $pop->pop->id])?>" class="text-danger" title="Löschen">
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameHerstellerPreismax. Leistungerstellt von
NameHerstellerPreis
name ?>devicemanufactor->name ?>price ?> €power ?> Wattcreator->name ?> + (create) ?>) + + $devicetype->id]) ?>"> + $devicetype->id]) ?>" + onclick="if(!confirm('Gerätetyp wirklich löschen?')) return false;" class="text-danger" + title="Löschen"> +
+ + + + + + + + + + + +
+ ">Bearbeiten
- - -
"> -
-
- +
-
-
- -
- - +

Passwort ändern

+ "> +
+ +
+ +
-
- - "> - - +
+ +
+ +
-
+
+ +
+ +
+
+
+
+ +
+
+ +
+

2FA

+ +
"> +
+ + +
+ +
+
+
+ +
"> +
+
+ - - +
+
+ +
+ + +
+ +
+ + + +
+
diff --git a/Layout/default/footer.php b/Layout/default/footer.php index 98957be73..e18e6fa8b 100644 --- a/Layout/default/footer.php +++ b/Layout/default/footer.php @@ -22,6 +22,20 @@ tickIcon: "check", sanitize: false }); + $('.navbar-toggle').on('click', function (event) { + console.log('cracy'); + $(this).toggleClass('open'); + $('#navigation').slideToggle(400); + }); + + $('.navigation-menu>li').slice(-2).addClass('last-elements'); + + $('.navigation-menu li.has-submenu a[href="#"]').on('click', function (e) { + if ($(window).width() < 992) { + e.preventDefault(); + $(this).parent('li').toggleClass('open').find('.submenu:first').toggleClass('open'); + } + }); \ No newline at end of file diff --git a/Layout/default/header.php b/Layout/default/header.php index caaa63ccb..123d80019 100644 --- a/Layout/default/header.php +++ b/Layout/default/header.php @@ -19,7 +19,7 @@ - + @@ -32,7 +32,7 @@ - + @@ -144,6 +154,14 @@ if ($requesttype == "2fa" || $requesttype == "false2fa") { document.getElementById('mfUsername').focus(); } + $('body').on('click', '.form-check-label', function (event) { + if ($('Remember').prop("checked")) { + $('Remember').prop("checked", false); + } else { + $('Remember').prop("checked", true); + } + }); + $('body').on('click', '#new-authcode', function (event) { $('#TwofactorCode').remove(); $('form').submit(); diff --git a/application/Network/Network.php b/application/Network/Network.php index e783d69a7..22db71547 100644 --- a/application/Network/Network.php +++ b/application/Network/Network.php @@ -114,7 +114,7 @@ class Network extends mfBaseModel { } if($name == "pops") { - $pops = PopModel::search(['network_id' => $this->id]); + $pops = PopNetworkModel::search(['network_id' => $this->id]); $this->pops = $pops; return $this->pops; } diff --git a/application/Pop/PopController.php b/application/Pop/PopController.php index a1ff0ca09..f49ea37fd 100644 --- a/application/Pop/PopController.php +++ b/application/Pop/PopController.php @@ -21,7 +21,8 @@ class PopController extends mfBaseController { $this->layout()->setTemplate("Pop/Index"); - $pops = PopModel::getAll(); + $pops = PopModel::getAlladv(); + $this->layout()->set("pops", $pops); } @@ -40,7 +41,8 @@ class PopController extends mfBaseController $this->layout()->setFlash("Pop nicht gefunden", "error"); $this->redirect("Pop"); } - + $popnetwork = PopNetworkModel::getbyPopid($id); + $this->layout()->set("popnetwork", implode(', ' , $popnetwork['name'])); $this->layout()->setTemplate("Pop/Detail"); $filter['pop_id'] = $id; $this->layout()->set("popracks", PoprackModel::getAllbyPop($id)); @@ -76,6 +78,8 @@ class PopController extends mfBaseController $this->redirect("Network"); } + $popnetwork = PopNetworkModel::getbyPopid($id); + $this->layout()->set("popnetwork", $popnetwork['network_id']); $this->layout()->set("pop", $pop); return $this->addAction(); } @@ -106,7 +110,8 @@ class PopController extends mfBaseController } $data = []; - $data['network_id'] = $r->network_id; + + $data['name'] = $r->name; $data['gps_lat'] = ($r->gps_lat) ? $r->gps_lat : null; $data['gps_long'] = ($r->gps_long) ? $r->gps_long : null; @@ -127,11 +132,34 @@ class PopController extends mfBaseController $new_id = $pop->save(); + + if (!$new_id) { $this->layout()->setFlash("Fehler beim Speichern", "error"); $this->layout()->set("network", $network); return $this->addAction(); } + + if ($r->network_id) { + $oldPopnetworks = json_encode(PopNetworkModel::getbyPopid($new_id)); + + + $result = array_diff($r->network_id, $oldPopnetworks); + + if ($oldPopnetworks != json_encode($r->network_id)) { + PopNetworkModel::deletebyPopid($new_id); + unset($data); + $data = []; + foreach ($r->network_id as $networkid) { + $data['network_id'] = $networkid; + $data['pop_id'] = $new_id; + $popNetwork = PopNetworkModel::create($data); + $popNetwork->save(); + } + } + } + + if ($this->request->returnto) { $returnAction = "Index"; $returnVariables = array(); diff --git a/application/Pop/PopModel.php b/application/Pop/PopModel.php index e7829d417..29f664ca4 100644 --- a/application/Pop/PopModel.php +++ b/application/Pop/PopModel.php @@ -1,130 +1,163 @@ $value) { - if(property_exists(get_called_class(), $field)) { - if(substr($field, 0, 5) == "vlan_" && !$value) { - $model->$field = null; - continue; +class PopModel +{ + public $name = null; + public $network_id = null; + public $gps_lat = null; + public $gps_long = null; + public $location = null; + public $vlan_public = null; + public $vlan_nat = null; + public $vlan_ipv6 = null; + + public $note = null; + + public $create_by = null; + public $edit_by = null; + public $create = null; + public $edit = null; + + public static function find($data) + { + + } + + public static function create(array $data) + { + $model = new Pop(); + + foreach ($data as $field => $value) { + if (property_exists(get_called_class(), $field)) { + if (substr($field, 0, 5) == "vlan_" && !$value) { + $model->$field = null; + continue; + } + $model->$field = $value; + } } - $model->$field = $value; - } - } - - $me = mfValuecache::singleton()->get("me"); - if(!$me) { - $me = new User(); - $me->loadMe(); - mfValuecache::singleton()->set("me", $me); + + $me = mfValuecache::singleton()->get("me"); + if (!$me) { + $me = new User(); + $me->loadMe(); + mfValuecache::singleton()->set("me", $me); + } + + if ($model->create_by === null) { + $model->create_by = $me->id; + } + if ($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; } - if($model->create_by === null) { - $model->create_by = $me->id; - } - if($model->edit_by === null) { - $model->edit_by = $me->id; - } - - return $model; - } - - public static function getOne($id) { - if(!is_numeric($id) || !$id) { - throw new Exception("Invalid number", 400); - } - $item = []; - $db = FronkDB::singleton(); - - $res = $db->select("Pop", "*", "id=$id LIMIT 1"); - if($db->num_rows($res)) { - $data = $db->fetch_object($res); - $item = new Pop($data); - } - return $item; - } - - public static function getAll() { - $items = []; - - $db = FronkDB::singleton(); - - $res = $db->select("Pop", "*","1=1 ORDER by name"); - if($db->num_rows($res)) { - while($data = $db->fetch_object($res)) { - $items[] = new Pop($data); - } - } - return $items; - - } - - public static function getFirst() { - $db = FronkDB::singleton(); - - $where = self::getSqlFilter($filter); - $res = $db->select("Pop", "*", "$where ORDER BY name, network_id"); - if($db->num_rows($res)) { - $data = $db->fetch_object($res); - $item = new Pop($data); - if($item->id) { + public static function getOne($id) + { + if (!is_numeric($id) || !$id) { + throw new Exception("Invalid number", 400); + } + $item = []; + $db = FronkDB::singleton(); + + $res = $db->select("Pop", "*", "id=$id LIMIT 1"); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new Pop($data); + } return $item; - } else { - return null; - } } - return null; - } - - public static function search($filter) { - $items = []; - $db = FronkDB::singleton(); - - $where = self::getSqlFilter($filter); - $res = $db->select("Pop", "*", "$where ORDER BY name, network_id"); - if($db->num_rows($res)) { - while($data = $db->fetch_object($res)) { - $items[] = new Pop($data); - } - } - return $items; - } - - private static function getSqlFilter($filter) { - $where = "1=1 "; - //var_dump($filter);exit; - if(array_key_exists("network_id", $filter)) { - $networkid = $filter['network_id']; - if(is_numeric($networkid)) { - $where .= " AND network_id=$networkid"; - } + public static function getAll() + { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("Pop", "*", "1=1 ORDER by name"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new Pop($data); + } + } + return $items; + } - - //var_dump($filter, $where);exit; - return $where; - } - + + public static function getAlladv() + { + $items = []; + $db = FronkDB::singleton(); + $sql = "SELECT `Pop`.`id`, `Pop`.`network_id`, `Pop`.`name`, `Pop`.`gps_lat`, `Pop`.`gps_long`, `Pop`.`location`, `Pop`.`vlan_public`, `Pop`.`vlan_nat`, `Pop`.`vlan_ipv6`, `Pop`.`note`,`Network`.`name` as Networkname FROM `Pop` + INNER JOIN `PopNetwork` ON (`Pop`.`id`=`PopNetwork`.`pop_id`) + INNER JOIN `Network` ON (`Network`.`id`=`PopNetwork`.`network_id`)"; + $res = $db->query($sql); + if ($db->num_rows($res)) { + while ($data = $db->fetch_array($res)) { + $Pops[$data['id']]['data'] = $data; + $Pops[$data['id']]['networks'][] = $data['Networkname']; + } + } + foreach ($Pops as $key => $Pop) { + $Pop['data']['networks'] = implode(', ', $Pop['networks']); + $items[] = new Pop((object)$Pop['data']); + } + + + return $items; + + + } + + public static function getFirst() + { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("Pop", "*", "$where ORDER BY name, network_id"); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new Pop($data); + if ($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function search($filter) + { + $items = []; + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("Pop", "*", "$where ORDER BY name, network_id"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new Pop($data); + } + } + return $items; + } + + private static function getSqlFilter($filter) + { + $where = "1=1 "; + + //var_dump($filter);exit; + if (array_key_exists("network_id", $filter)) { + $networkid = $filter['network_id']; + if (is_numeric($networkid)) { + $where .= " AND network_id=$networkid"; + } + } + + //var_dump($filter, $where);exit; + return $where; + } + } diff --git a/application/PopNetwork/PopNetwork.php b/application/PopNetwork/PopNetwork.php new file mode 100644 index 000000000..c3cf701a3 --- /dev/null +++ b/application/PopNetwork/PopNetwork.php @@ -0,0 +1,44 @@ +$name == null) { + + if (!$this->id) { + return null; + } + + if ($name == "creator") { + $this->creator = new User($this->create_by); + return $this->creator; + } + + if ($name == "editor") { + $this->editor = new User($this->edit_by); + return $this->editor; + } + + $classname = ucfirst($name); + $idfield = $name . "_id"; + $this->$name = new $classname($this->$idfield); + + if ($this->$name->id) { + return $this->$name; + } else { + return null; + } + } + + return $this->$name; + } + +} \ No newline at end of file diff --git a/application/PopNetwork/PopNetworkModel.php b/application/PopNetwork/PopNetworkModel.php new file mode 100644 index 000000000..63fe70a2b --- /dev/null +++ b/application/PopNetwork/PopNetworkModel.php @@ -0,0 +1,154 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + if (substr($field, 0, 5) == "vlan_" && !$value) { + $model->$field = null; + continue; + } + $model->$field = $value; + } + } + + $me = mfValuecache::singleton()->get("me"); + if (!$me) { + $me = new User(); + $me->loadMe(); + mfValuecache::singleton()->set("me", $me); + } + + if ($model->create_by === null) { + $model->create_by = $me->id; + } + if ($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getOne($id) + { + if (!is_numeric($id) || !$id) { + throw new Exception("Invalid number", 400); + } + $item = []; + $db = FronkDB::singleton(); + + $res = $db->select("PopNetwork", "*", "id=$id LIMIT 1"); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new PopNetwork($data); + } + return $item; + } + + public static function getAll() + { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("PopNetwork", "*", "1=1"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new PopNetwork($data); + } + } + return $items; + + } + + public static function getbyPopid($pop_id) + { + $items = []; + + $db = FronkDB::singleton(); + + $sql = "SELECT `PopNetwork`.`id`,`PopNetwork`.`network_id`,`Network`.`name` FROM `PopNetwork` + INNER JOIN `Network` ON (`Network`.`id`=`PopNetwork`.`network_id`) + WHERE `PopNetwork`.`pop_id`='" . $pop_id . "'"; + $res = $db->query($sql); + if ($db->num_rows($res)) { + while ($data = $db->fetch_array($res)) { + $items['network_id'][] = $data['network_id']; + $items['name'][] = $data['name']; + } + } + return $items; + + } + + public static function deletebyPopid($pop_id) + { + $db = FronkDB::singleton(); + $sql = "DELETE FROM `PopNetwork` WHERE `pop_id`='" . $pop_id . "'"; + $db->query($sql); + + } + + public static function getFirst() + { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("PopNetwork", "*", "$where "); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new PopNetwork($data); + if ($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function search($filter) + { + $items = []; + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("PopNetwork", "*", "$where"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new PopNetwork($data); + } + } + return $items; + } + + private static function getSqlFilter($filter) + { + $where = "1=1 "; + + //var_dump($filter);exit; + if (array_key_exists("network_id", $filter)) { + $networkid = $filter['network_id']; + if (is_numeric($networkid)) { + $where .= " AND network_id=$networkid"; + } + } + + //var_dump($filter, $where);exit; + return $where; + } + +} diff --git a/application/Poprack/PoprackModel.php b/application/Poprack/PoprackModel.php index 29e479b3d..0980ea6fe 100644 --- a/application/Poprack/PoprackModel.php +++ b/application/Poprack/PoprackModel.php @@ -173,6 +173,28 @@ class PoprackModel $response['success'] = true; + } else { + $response['success'] = false; + } + echo json_encode($response); + exit; + } + public static function getdispatchersleeve($dispatcher_id) + { + $items = []; + $db = FronkDB::singleton(); + $sql = "SELECT `id`, `name` FROM `FiberPlanDispatchersleeve` WHERE fiberPlanDispatcher_id='" . $dispatcher_id . "' "; + + $res = $db->query($sql); + if ($db->num_rows($res)) { + while ($data = $db->fetch_array($res)) { + $items[] = $data; + } + + $response['data'] = $items; + $response['success'] = true; + + } else { $response['success'] = false; } diff --git a/application/Poprackmodule/PoprackmoduleController.php b/application/Poprackmodule/PoprackmoduleController.php index 78d686742..8f818e1ee 100644 --- a/application/Poprackmodule/PoprackmoduleController.php +++ b/application/Poprackmodule/PoprackmoduleController.php @@ -33,6 +33,7 @@ class PoprackmoduleController extends mfBaseController private function addModule() { + $ports = 0; $r = $this->request; $id = $r->id; $data = []; @@ -40,7 +41,9 @@ class PoprackmoduleController extends mfBaseController $data['type'] = $r->type; if ($data['type'] == 0) { $data['ports'] = ($r->ports) ? $r->ports : null; + $ports = $data['ports']; $data['plug'] = ($r->plug) ? $r->plug : null; + } if ($data['type'] == 1) { $data['device_id'] = ($r->device_id) ? $r->device_id : null; @@ -59,6 +62,16 @@ class PoprackmoduleController extends mfBaseController $response['success'] = false; } else { $response['success'] = true; + if ($data['type'] == 0) { + unset($data); + $data = []; + $data['poprackmodule_id'] = $new_id; + for ($i = 1; $i <= $ports; $i++) { + $data['port'] = $i; + $poprackmodulepatch = PoprackmodulepatchModel::create($data); + $poprackmodulepatch->save(); + } + } } echo json_encode($response); exit; @@ -82,12 +95,26 @@ class PoprackmoduleController extends mfBaseController if ($r->type == 0) { $data['ports'] = ($r->ports) ? $r->ports : null; $data['plug'] = ($r->plug) ? $r->plug : null; + $poprackmodulepatchcounter = PoprackmodulepatchModel::countAllModule($id); + if ($poprackmodulepatchcounter > $data['ports']) { + for ($i = $poprackmodulepatchcounter; $i > $data['ports']; $i--) { + PoprackmodulepatchModel::deletebyPort($id, $i); + } + + } else if ($poprackmodulepatchcounter < $data['ports']) { + $datapatchModel=[]; + for ($i = $poprackmodulepatchcounter + 1; $i <= $data['ports']; $i++) { + $datapatchModel['poprackmodule_id'] = $id; + $datapatchModel['port'] = $i; + $poprackmodulepatch = PoprackmodulepatchModel::create($datapatchModel); + $poprackmodulepatch->save(); + } + + } } $data['name'] = ($r->name); $poprackmodule->update($data); - $new_id = $poprackmodule->save(); -// var_dump($r);die(); if (!$new_id) { $response['success'] = false; } else { diff --git a/application/Poprackmodulepatch/Poprackmodulepatch.php b/application/Poprackmodulepatch/Poprackmodulepatch.php new file mode 100644 index 000000000..fb6f21ea7 --- /dev/null +++ b/application/Poprackmodulepatch/Poprackmodulepatch.php @@ -0,0 +1,44 @@ +$name == null) { + + if (!$this->id) { + return null; + } + + if ($name == "creator") { + $this->creator = new User($this->create_by); + return $this->creator; + } + + if ($name == "editor") { + $this->editor = new User($this->edit_by); + return $this->editor; + } + + $classname = ucfirst($name); + $idfield = $name . "_id"; + $this->$name = new $classname($this->$idfield); + + if ($this->$name->id) { + return $this->$name; + } else { + return null; + } + } + + return $this->$name; + } + +} \ No newline at end of file diff --git a/application/Poprackmodulepatch/PoprackmodulepatchModel.php b/application/Poprackmodulepatch/PoprackmodulepatchModel.php new file mode 100644 index 000000000..c80c2c01a --- /dev/null +++ b/application/Poprackmodulepatch/PoprackmodulepatchModel.php @@ -0,0 +1,108 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + if (substr($field, 0, 5) == "vlan_" && !$value) { + $model->$field = null; + continue; + } + $model->$field = $value; + } + } + + $me = mfValuecache::singleton()->get("me"); + if (!$me) { + $me = new User(); + $me->loadMe(); + mfValuecache::singleton()->set("me", $me); + } + + if ($model->create_by === null) { + $model->create_by = $me->id; + } + if ($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getOne($id) + { + if (!is_numeric($id) || !$id) { + throw new Exception("Invalid number", 400); + } + $item = []; + $db = FronkDB::singleton(); + + $res = $db->select("Poprackmodulepatch", "*", "id=$id LIMIT 1"); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new Poprackmodulepatch($data); + } + return $item; + } + + public static function countAllModule($moduleId) + { + $items = []; + $db = FronkDB::singleton(); + $sql = "SELECT `id` FROM `Poprackmodulepatch` WHERE `poprackmodule_id`='" . $moduleId . "'"; + + $res = $db->query($sql); + $countrows = $db->num_rows($res); + return $countrows; + } + + public static function deletebyPort($moduleId, $port) + { + $db = FronkDB::singleton(); + $sql = "DELETE FROM `Poprackmodulepatch` WHERE `port`='" . $port . "' AND `poprackmodule_id`='" . $moduleId . "'"; + $res = $db->query($sql); + } + + public static function updatebyPort($moduleId, $port, $fiberPlanCable_id) + { + $db = FronkDB::singleton(); + $sql = "UPDATE `Poprackmodulepatch` SET fiberPlanCable_id='" . $fiberPlanCable_id . "' WHERE `port`='" . $port . "' AND `poprackmodule_id`='" . $moduleId . "'"; + echo $sql . "\n"; + $db->query($sql); + } + + public static function updatebyPortRange($moduleId, $startport, $endport, $fiberPlanCable_id) + { + $db = FronkDB::singleton(); + $sql = "UPDATE `Poprackmodulepatch` SET fiberPlanCable_id='" . $fiberPlanCable_id . "' WHERE `port`>='" . $startport . "' AND `port`<='" . $endport . "' AND `poprackmodule_id`='" . $moduleId . "'"; + echo $sql . "\n"; + $db->query($sql); + } + + public static function clearPort($fiberPlanCable_id) + { + $db = FronkDB::singleton(); + $sql = "UPDATE `Poprackmodulepatch` SET fiberPlanCable_id=NULL WHERE `fiberPlanCable_id`='" . $fiberPlanCable_id . "'"; + $db->query($sql); + } + + +} diff --git a/db/migrations/20231219144324_poprackmodulepatch.php b/db/migrations/20231219144324_poprackmodulepatch.php new file mode 100644 index 000000000..3d0a9c74b --- /dev/null +++ b/db/migrations/20231219144324_poprackmodulepatch.php @@ -0,0 +1,41 @@ +getEnvironment() == "thetool") { + $poprackmodulepatch = $this->table("Poprackmodulepatch", ['signed' => true]); + $poprackmodulepatch->addColumn("poprackmodule_id", "integer", ["null" => false]); + $poprackmodulepatch->addColumn("port", "integer", ["null" => false])->addIndex(['port']); + $poprackmodulepatch->addColumn("fiberPlanCable_id", "integer", ["null" => true, "default" => null])->addIndex(['fiberPlanCable_id']); + $poprackmodulepatch->addColumn("destination", "integer", ["null" => true, "default" => null, "comment" => "1:startpoint/2:endpoint"]); + $poprackmodulepatch->addColumn("create_by", "integer", ["null" => false]); + $poprackmodulepatch->addColumn("edit_by", "integer", ["null" => false]); + $poprackmodulepatch->addColumn("create", "integer", ["null" => false]); + $poprackmodulepatch->addColumn("edit", "integer", ["null" => false]); + $poprackmodulepatch->save(); + $poprackmodulepatch = $this->table("Poprackmodulepatch"); + $poprackmodulepatch->addForeignKey('poprackmodule_id', 'Poprackmodule', ['id'],['delete'=> 'CASCADE', 'update'=> 'RESTRICT','constraint' => 'poprackmodule_id']); + $poprackmodulepatch->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("Poprackmodulepatch")->drop()->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/db/migrations/20240101121050_pop_network.php b/db/migrations/20240101121050_pop_network.php new file mode 100644 index 000000000..f2398fd43 --- /dev/null +++ b/db/migrations/20240101121050_pop_network.php @@ -0,0 +1,36 @@ +getEnvironment() == "thetool") { + $popnetwork = $this->table("PopNetwork", ['signed' => true]); + $popnetwork->addColumn("pop_id", "integer", ["null" => false])->addIndex(['pop_id']); + $popnetwork->addColumn("network_id", "integer", ["null" => false])->addIndex(['network_id']); + $popnetwork->addColumn("create_by", "integer", ["null" => false]); + $popnetwork->addColumn("edit_by", "integer", ["null" => false]); + $popnetwork->addColumn("create", "integer", ["null" => false]); + $popnetwork->addColumn("edit", "integer", ["null" => false]); + $popnetwork->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("PopNetwork")->drop()->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/public/assets/css/app.css b/public/assets/css/app.css index 02c5824ba..df465dd7f 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -442,7 +442,12 @@ Responsive Menu background-color: #fff; } #navigation.open { display: block; - overflow-y: auto; } } + overflow-y: auto; } + .mobile-hide + { + display:none !important; + } +} @media (max-height: 375px) { .navigation-menu { @@ -1982,3 +1987,7 @@ body.authentication-bg { .social-links li a:hover { color: #6c757d; border-color: #6c757d; } +.d-lg-list-item +{ + display: list-item; +} \ No newline at end of file diff --git a/public/assets/css/datatables-std.css b/public/assets/css/datatables-std.css index fd8c02a2a..2eaac518a 100644 --- a/public/assets/css/datatables-std.css +++ b/public/assets/css/datatables-std.css @@ -15,6 +15,20 @@ float: right; } +table.dataTable > tbody > tr.child ul.dtr-details { + width: 100%; +} + +table.dataTable > tbody > tr.child span.dtr-title { + + display: table-row; +} + +table.dataTable > tbody > tr.child span.dtr-data { + display: table-cell; + text-wrap: wrap; +} + .clear-fa { margin-left: 5px; font-size: 23px; @@ -87,21 +101,96 @@ .w-30 { width: 33% !important; } -.fa-ban -{ + +.fa-ban { color: #0151e7; font-size: 15px; } -.fa-circle-check -{ + +.fa-circle-check { color: #23b900; font-size: 15px; } -.fa-circle-xmark -{ + +.fa-circle-xmark { color: #f1556c; font-size: 15px; } -table.dataTable.table-sm>thead>tr>th:not(.sorting_disabled) { - padding-right:.85rem ; + +table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) { + padding-right: .85rem; } + +.mobile { + width: unset; + display: table-cell; +} + +@media (max-width: 1200px) { + .fa-circle-xmark, .fa-ban, .fa-trash, .fa-edit, .fa-arrows-up-down-left-right { + font-size: 25px; + } + + .fa-trash { + float: right; + } + + .mobile { + width: 0px; + display: none; + } + + .card-body { + padding-left: 5px; + padding-right: 5px; + } + + .form-control { + margin-top: 4px; + } + + .alert { + margin-top: 4px; + text-align: center; + } + + .check-button { + margin-top: 20px; + } + + #filterrow { + display: none; + } + + #datatable { + width: 100% !important; + } +} + +table.dataTable > tbody > tr.child span.dtr-data { + text-wrap: wrap; + display: inline-block; + width: 100%; +} + +table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) { + padding-right: 1.5rem; +} + +table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td.dtr-control:before, table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th.dtr-control:before { + content: "▼"; + +} + +table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control:before, table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control:before { + margin-right: 0.5em; + display: inline-block; + color: rgba(0, 0, 0, 0.5); + content: "►"; +} + +table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td:first-child:before, table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th:first-child:before { + background-color: unset; + top: 0.85rem; +} + diff --git a/public/datatables/datatables.css b/public/datatables/datatables.css index 831df59a7..d7cec60e0 100644 --- a/public/datatables/datatables.css +++ b/public/datatables/datatables.css @@ -4,17 +4,24 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/jszip-2.5.0/pdfmake-0.1.36/dt-1.13.2/b-2.3.4/b-html5-2.3.4 + * https://datatables.net/download/#bs5/jszip-3.10.1/pdfmake-0.2.7/dt-1.13.8/b-2.4.2/b-html5-2.4.2/b-print-2.4.2/r-2.5.0 * * Included libraries: - * JSZip 2.5.0, pdfmake 0.1.36, DataTables 1.13.2, Buttons 2.3.4, HTML5 export 2.3.4 + * JSZip 3.10.1, pdfmake 0.2.7, DataTables 1.13.8, Buttons 2.4.2, HTML5 export 2.4.2, Print view 2.4.2, Responsive 2.5.0 */ @charset "UTF-8"; :root { - --dt-row-selected: 2, 117, 216; + --dt-row-selected: 13, 110, 253; --dt-row-selected-text: 255, 255, 255; --dt-row-selected-link: 9, 10, 11; + --dt-row-stripe: 0, 0, 0; + --dt-row-hover: 0, 0, 0; + --dt-column-ordering: 0, 0, 0; + --dt-html-background: white; +} +:root.dark { + --dt-html-background: rgb(33, 37, 41); } table.dataTable td.dt-control { @@ -22,25 +29,19 @@ table.dataTable td.dt-control { cursor: pointer; } table.dataTable td.dt-control:before { - height: 1em; - width: 1em; - margin-top: -9px; display: inline-block; - color: white; - border: 0.15em solid white; - border-radius: 1em; - box-shadow: 0 0 0.2em #444; - box-sizing: content-box; - text-align: center; - text-indent: 0 !important; - font-family: "Courier New", Courier, monospace; - line-height: 1em; - content: "+"; - background-color: #31b131; + color: rgba(0, 0, 0, 0.5); + content: "▶"; } table.dataTable tr.dt-hasChild td.dt-control:before { - content: "-"; - background-color: #d33333; + content: "▼"; +} + +html.dark table.dataTable td.dt-control:before { + color: rgba(255, 255, 255, 0.5); +} +html.dark table.dataTable tr.dt-hasChild td.dt-control:before { + color: rgba(255, 255, 255, 0.5); } table.dataTable thead > tr > th.sorting, table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting_asc_disabled, table.dataTable thead > tr > th.sorting_desc_disabled, @@ -79,6 +80,7 @@ table.dataTable thead > tr > td.sorting_asc_disabled:before, table.dataTable thead > tr > td.sorting_desc_disabled:before { bottom: 50%; content: "▲"; + content: "▲"/""; } table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > td.sorting:after, @@ -88,6 +90,7 @@ table.dataTable thead > tr > td.sorting_asc_disabled:after, table.dataTable thead > tr > td.sorting_desc_disabled:after { top: 50%; content: "▼"; + content: "▼"/""; } table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > td.sorting_asc:before, @@ -104,9 +107,9 @@ table.dataTable thead > tr > td:active { outline: none; } -div.dataTables_scrollBody table.dataTable thead > tr > th:before, div.dataTables_scrollBody table.dataTable thead > tr > th:after, -div.dataTables_scrollBody table.dataTable thead > tr > td:before, -div.dataTables_scrollBody table.dataTable thead > tr > td:after { +div.dataTables_scrollBody > table.dataTable > thead > tr > th:before, div.dataTables_scrollBody > table.dataTable > thead > tr > th:after, +div.dataTables_scrollBody > table.dataTable > thead > tr > td:before, +div.dataTables_scrollBody > table.dataTable > thead > tr > td:after { display: none; } @@ -119,6 +122,7 @@ div.dataTables_processing { margin-top: -26px; text-align: center; padding: 2px; + z-index: 10; } div.dataTables_processing > div:last-child { position: relative; @@ -132,7 +136,8 @@ div.dataTables_processing > div:last-child > div { width: 13px; height: 13px; border-radius: 50%; - background: 2 117 216; + background: rgb(13, 110, 253); + background: rgb(var(--dt-row-selected)); animation-timing-function: cubic-bezier(0, 1, 1, 0); } div.dataTables_processing > div:last-child > div:nth-child(1) { @@ -257,6 +262,11 @@ table.dataTable tbody td.dt-body-nowrap { white-space: nowrap; } +/*! Bootstrap 5 integration for DataTables + * + * ©2020 SpryMedia Ltd, all rights reserved. + * License: MIT datatables.net/license/mit + */ table.dataTable { clear: both; margin-top: 6px !important; @@ -278,14 +288,14 @@ table.dataTable.nowrap th, table.dataTable.nowrap td { white-space: nowrap; } -table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) { - background-color: transparent; +table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { + box-shadow: none; } table.dataTable > tbody > tr { background-color: transparent; } table.dataTable > tbody > tr.selected > * { - box-shadow: inset 0 0 0 9999px rgb(2, 117, 216); + box-shadow: inset 0 0 0 9999px rgb(13, 110, 253); box-shadow: inset 0 0 0 9999px rgb(var(--dt-row-selected)); color: rgb(255, 255, 255); color: rgb(var(--dt-row-selected-text)); @@ -294,18 +304,18 @@ table.dataTable > tbody > tr.selected a { color: rgb(9, 10, 11); color: rgb(var(--dt-row-selected-link)); } -table.dataTable.table-striped > tbody > tr.odd > * { - box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); +table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1) > * { + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05); } -table.dataTable.table-striped > tbody > tr.odd.selected > * { - box-shadow: inset 0 0 0 9999px rgba(2, 117, 216, 0.95); +table.dataTable.table-striped > tbody > tr:nth-of-type(2n+1).selected > * { + box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.95); box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95); } table.dataTable.table-hover > tbody > tr:hover > * { - box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075); } table.dataTable.table-hover > tbody > tr.selected:hover > * { - box-shadow: inset 0 0 0 9999px rgba(2, 117, 216, 0.975); + box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.975); box-shadow: inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975); } @@ -344,15 +354,8 @@ div.dataTables_wrapper div.dataTables_paginate ul.pagination { white-space: nowrap; justify-content: flex-end; } -div.dataTables_wrapper div.dataTables_processing { - position: absolute; - top: 50%; - left: 50%; - width: 200px; - margin-left: -100px; - margin-top: -26px; - text-align: center; - padding: 1em 0; +div.dataTables_wrapper div.dt-row { + position: relative; } div.dataTables_scrollHead table.dataTable { @@ -399,22 +402,34 @@ div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) { padding-right: 20px; } +table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled):before, table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled):after { + right: 5px; +} table.table-bordered.dataTable { border-right-width: 0; } +table.table-bordered.dataTable thead tr:first-child th, +table.table-bordered.dataTable thead tr:first-child td { + border-top-width: 1px; +} table.table-bordered.dataTable th, table.table-bordered.dataTable td { border-left-width: 0; } +table.table-bordered.dataTable th:first-child, table.table-bordered.dataTable th:first-child, +table.table-bordered.dataTable td:first-child, +table.table-bordered.dataTable td:first-child { + border-left-width: 1px; +} table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable td:last-child, table.table-bordered.dataTable td:last-child { border-right-width: 1px; } -table.table-bordered.dataTable tbody th, -table.table-bordered.dataTable tbody td { - border-bottom-width: 0; +table.table-bordered.dataTable th, +table.table-bordered.dataTable td { + border-bottom-width: 1px; } div.dataTables_scrollHead table.table-bordered { @@ -431,6 +446,12 @@ div.table-responsive > div.dataTables_wrapper > div.row > div[class^=col-]:last- padding-right: 0; } +:root[data-bs-theme=dark] { + --dt-row-hover: 255, 255, 255; + --dt-row-stripe: 255, 255, 255; + --dt-column-ordering: 255, 255, 255; +} + @keyframes dtb-spinner { 100% { @@ -481,31 +502,27 @@ div.dt-button-info { margin-top: -100px; margin-left: -200px; background-color: white; - border: 2px solid #111; - box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.3); - border-radius: 3px; + border-radius: 0.75em; + box-shadow: 3px 4px 10px 1px rgba(0, 0, 0, 0.8); text-align: center; - z-index: 21; + z-index: 2003; + overflow: hidden; } div.dt-button-info h2 { - padding: 0.5em; + padding: 2rem 2rem 1rem 2rem; margin: 0; font-weight: normal; - border-bottom: 1px solid #ddd; - background-color: #f3f3f3; } div.dt-button-info > div { - padding: 1em; + padding: 1em 2em 2em 2em; } div.dtb-popover-close { position: absolute; - top: 10px; - right: 10px; + top: 6px; + right: 6px; width: 22px; height: 22px; - border: 1px solid #eaeaea; - background-color: #f9f9f9; text-align: center; border-radius: 3px; cursor: pointer; @@ -544,6 +561,46 @@ span.dt-button-spacer.bar:empty { padding-left: 0; } +div.dt-button-collection .dt-button-active { + padding-right: 3em; +} +div.dt-button-collection .dt-button-active:after { + position: absolute; + top: 50%; + margin-top: -10px; + right: 1em; + display: inline-block; + content: "✓"; + color: inherit; +} +div.dt-button-collection .dt-button-active.dt-button-split { + padding-right: 0; +} +div.dt-button-collection .dt-button-active.dt-button-split:after { + display: none; +} +div.dt-button-collection .dt-button-active.dt-button-split > *:first-child { + padding-right: 3em; +} +div.dt-button-collection .dt-button-active.dt-button-split > *:first-child:after { + position: absolute; + top: 50%; + margin-top: -10px; + right: 1em; + display: inline-block; + content: "✓"; + color: inherit; +} +div.dt-button-collection .dt-button-active-a a { + padding-right: 3em; +} +div.dt-button-collection .dt-button-active-a a:after { + position: absolute; + right: 1em; + display: inline-block; + content: "✓"; + color: inherit; +} div.dt-button-collection span.dt-button-spacer { width: 100%; font-size: 0.9em; @@ -556,32 +613,55 @@ div.dt-button-collection span.dt-button-spacer:empty { } div.dt-button-collection span.dt-button-spacer.bar { border-left: none; - border-bottom: 1px solid rgba(0, 0, 0, 0.3); + border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding-left: 0; } -div.dt-button-collection { - position: absolute; - z-index: 2001; - background-color: white; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 4px; - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - padding: 0.5rem; - width: 218px; +html.dark div.dt-button-info { + background-color: var(--dt-html-background); + border: 1px solid rgba(255, 255, 255, 0.15); } -div.dt-button-collection div.dropdown-menu { + +div.dt-buttons div.btn-group { + position: initial; +} +div.dt-buttons div.dropdown-menu { + margin-top: 4px; +} +div.dt-buttons div.dropdown-menu .dt-button { position: relative; - display: block; - z-index: 2002; - min-width: 100%; - background-color: transparent; - border: none; - box-shadow: none; - padding: 0; - border-radius: 0; } -div.dt-button-collection.fixed { +div.dt-buttons div.dropdown-menu div.dt-button-split { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + align-content: flex-start; + align-items: stretch; +} +div.dt-buttons div.dropdown-menu div.dt-button-split a:first-child { + min-width: auto; + flex: 1 0 50px; + padding-right: 0; +} +div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child { + min-width: 33px; + flex: 0; + background: transparent; + border: none; + line-height: 1rem; + color: var(--bs-dropdown-link-color); + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); +} +div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:hover { + color: var(--bs-dropdown-link-hover-color); + background-color: var(--bs-dropdown-link-hover-bg); +} +div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:after { + position: relative; + left: -3px; +} +div.dt-buttons div.dropdown-menu.fixed { position: fixed; display: block; top: 50%; @@ -589,39 +669,40 @@ div.dt-button-collection.fixed { margin-left: -75px; border-radius: 5px; background-color: white; + padding: 0.5em; } -div.dt-button-collection.fixed.two-column { +div.dt-buttons div.dropdown-menu.fixed.two-column { margin-left: -200px; } -div.dt-button-collection.fixed.three-column { +div.dt-buttons div.dropdown-menu.fixed.three-column { margin-left: -225px; } -div.dt-button-collection.fixed.four-column { +div.dt-buttons div.dropdown-menu.fixed.four-column { margin-left: -300px; } -div.dt-button-collection.fixed.columns { +div.dt-buttons div.dropdown-menu.fixed.columns { margin-left: -409px; } @media screen and (max-width: 1024px) { - div.dt-button-collection.fixed.columns { + div.dt-buttons div.dropdown-menu.fixed.columns { margin-left: -308px; } } @media screen and (max-width: 640px) { - div.dt-button-collection.fixed.columns { + div.dt-buttons div.dropdown-menu.fixed.columns { margin-left: -203px; } } @media screen and (max-width: 460px) { - div.dt-button-collection.fixed.columns { + div.dt-buttons div.dropdown-menu.fixed.columns { margin-left: -100px; } } -div.dt-button-collection.fixed > :last-child { +div.dt-buttons div.dropdown-menu.fixed > :last-child { max-height: 100vh; overflow: auto; } -div.dt-button-collection.two-column > :last-child, div.dt-button-collection.three-column > :last-child, div.dt-button-collection.four-column > :last-child { +div.dt-buttons div.dropdown-menu.two-column > :last-child, div.dt-buttons div.dropdown-menu.three-column > :last-child, div.dt-buttons div.dropdown-menu.four-column > :last-child { display: block !important; -webkit-column-gap: 8px; -moz-column-gap: 8px; @@ -629,38 +710,38 @@ div.dt-button-collection.two-column > :last-child, div.dt-button-collection.thre -o-column-gap: 8px; column-gap: 8px; } -div.dt-button-collection.two-column > :last-child > *, div.dt-button-collection.three-column > :last-child > *, div.dt-button-collection.four-column > :last-child > * { +div.dt-buttons div.dropdown-menu.two-column > :last-child > *, div.dt-buttons div.dropdown-menu.three-column > :last-child > *, div.dt-buttons div.dropdown-menu.four-column > :last-child > * { -webkit-column-break-inside: avoid; break-inside: avoid; } -div.dt-button-collection.two-column { +div.dt-buttons div.dropdown-menu.two-column { width: 400px; } -div.dt-button-collection.two-column > :last-child { +div.dt-buttons div.dropdown-menu.two-column > :last-child { padding-bottom: 1px; column-count: 2; } -div.dt-button-collection.three-column { +div.dt-buttons div.dropdown-menu.three-column { width: 450px; } -div.dt-button-collection.three-column > :last-child { +div.dt-buttons div.dropdown-menu.three-column > :last-child { padding-bottom: 1px; column-count: 3; } -div.dt-button-collection.four-column { +div.dt-buttons div.dropdown-menu.four-column { width: 600px; } -div.dt-button-collection.four-column > :last-child { +div.dt-buttons div.dropdown-menu.four-column > :last-child { padding-bottom: 1px; column-count: 4; } -div.dt-button-collection .dt-button { +div.dt-buttons div.dropdown-menu .dt-button { border-radius: 0; } -div.dt-button-collection.columns { +div.dt-buttons div.dropdown-menu.columns { width: auto; } -div.dt-button-collection.columns > :last-child { +div.dt-buttons div.dropdown-menu.columns > :last-child { display: flex; flex-wrap: wrap; justify-content: flex-start; @@ -669,73 +750,69 @@ div.dt-button-collection.columns > :last-child { width: 818px; padding-bottom: 1px; } -div.dt-button-collection.columns > :last-child .dt-button { +div.dt-buttons div.dropdown-menu.columns > :last-child .dt-button { min-width: 200px; flex: 0 1; margin: 0; } -div.dt-button-collection.columns.dtb-b3 > :last-child, div.dt-button-collection.columns.dtb-b2 > :last-child, div.dt-button-collection.columns.dtb-b1 > :last-child { +div.dt-buttons div.dropdown-menu.columns.dtb-b3 > :last-child, div.dt-buttons div.dropdown-menu.columns.dtb-b2 > :last-child, div.dt-buttons div.dropdown-menu.columns.dtb-b1 > :last-child { justify-content: space-between; } -div.dt-button-collection.columns.dtb-b3 .dt-button { +div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button { flex: 1 1 32%; } -div.dt-button-collection.columns.dtb-b2 .dt-button { +div.dt-buttons div.dropdown-menu.columns.dtb-b2 .dt-button { flex: 1 1 48%; } -div.dt-button-collection.columns.dtb-b1 .dt-button { +div.dt-buttons div.dropdown-menu.columns.dtb-b1 .dt-button { flex: 1 1 100%; } @media screen and (max-width: 1024px) { - div.dt-button-collection.columns > :last-child { + div.dt-buttons div.dropdown-menu.columns > :last-child { width: 612px; } } @media screen and (max-width: 640px) { - div.dt-button-collection.columns > :last-child { + div.dt-buttons div.dropdown-menu.columns > :last-child { width: 406px; } - div.dt-button-collection.columns.dtb-b3 .dt-button { + div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button { flex: 0 1 32%; } } @media screen and (max-width: 460px) { - div.dt-button-collection.columns > :last-child { + div.dt-buttons div.dropdown-menu.columns > :last-child { width: 200px; } } -div.dt-button-collection.fixed:before, div.dt-button-collection.fixed:after { - display: none; +div.dt-buttons span.dt-button-spacer.empty { + margin: 1px; } -div.dt-button-collection .btn-group { - flex: 1 1 auto; +div.dt-buttons span.dt-button-spacer.bar:empty { + height: inherit; } -div.dt-button-collection .dt-button { - min-width: 200px; +div.dt-buttons .btn.processing { + color: rgba(0, 0, 0, 0.2); } -div.dt-button-collection div.dt-btn-split-wrapper { - width: 100%; - padding-left: 5px; - padding-right: 5px; -} -div.dt-button-collection button.dt-btn-split-drop-button { - width: 100%; - color: #212529; - border: none; - background-color: white; - border-radius: 0px; - margin-left: 0px !important; -} -div.dt-button-collection button.dt-btn-split-drop-button:focus { - border: none; - border-radius: 0px; - outline: none; -} -div.dt-button-collection button.dt-btn-split-drop-button:hover { - background-color: #e9ecef; -} -div.dt-button-collection button.dt-btn-split-drop-button:active { - background-color: #007bff !important; +div.dt-buttons .btn.processing:after { + position: absolute; + top: 50%; + left: 50%; + width: 16px; + height: 16px; + margin: -8px 0 0 -8px; + box-sizing: border-box; + display: block; + content: " "; + border: 2px solid rgb(40, 40, 40); + border-radius: 50%; + border-left-color: transparent; + border-right-color: transparent; + animation: dtb-spinner 1500ms infinite linear; + -o-animation: dtb-spinner 1500ms infinite linear; + -ms-animation: dtb-spinner 1500ms infinite linear; + -webkit-animation: dtb-spinner 1500ms infinite linear; + -moz-animation: dtb-spinner 1500ms infinite linear; } div.dt-button-background { @@ -758,109 +835,180 @@ div.dt-button-background { float: none; } } -div.dt-buttons button.btn.processing, -div.dt-buttons div.btn.processing, -div.dt-buttons a.btn.processing { - color: rgba(0, 0, 0, 0.2); +:root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed { + background-color: rgb(33, 37, 41); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; } -div.dt-buttons button.btn.processing:after, -div.dt-buttons div.btn.processing:after, -div.dt-buttons a.btn.processing:after { - position: absolute; - top: 50%; - left: 50%; - width: 16px; - height: 16px; - margin: -8px 0 0 -8px; + + +table.dataTable.dtr-inline.collapsed > tbody > tr > td.child, +table.dataTable.dtr-inline.collapsed > tbody > tr > th.child, +table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty { + cursor: default !important; +} +table.dataTable.dtr-inline.collapsed > tbody > tr > td.child:before, +table.dataTable.dtr-inline.collapsed > tbody > tr > th.child:before, +table.dataTable.dtr-inline.collapsed > tbody > tr > td.dataTables_empty:before { + display: none !important; +} +table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control, +table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control { + cursor: pointer; +} +table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control:before, +table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control:before { + margin-right: 0.5em; + display: inline-block; + color: rgba(0, 0, 0, 0.5); + content: "►"; +} +table.dataTable.dtr-inline.collapsed > tbody > tr > td.dtr-control.arrow-right::before, +table.dataTable.dtr-inline.collapsed > tbody > tr > th.dtr-control.arrow-right::before { + content: "◄"; +} +table.dataTable.dtr-inline.collapsed > tbody > tr.parent > td.dtr-control:before, +table.dataTable.dtr-inline.collapsed > tbody > tr.parent > th.dtr-control:before { + content: "▼"; +} +table.dataTable.dtr-inline.collapsed.compact > tbody > tr > td.dtr-control, +table.dataTable.dtr-inline.collapsed.compact > tbody > tr > th.dtr-control { + padding-left: 0.333em; +} +table.dataTable.dtr-column > tbody > tr > td.dtr-control, +table.dataTable.dtr-column > tbody > tr > th.dtr-control, +table.dataTable.dtr-column > tbody > tr > td.control, +table.dataTable.dtr-column > tbody > tr > th.control { + cursor: pointer; +} +table.dataTable.dtr-column > tbody > tr > td.dtr-control:before, +table.dataTable.dtr-column > tbody > tr > th.dtr-control:before, +table.dataTable.dtr-column > tbody > tr > td.control:before, +table.dataTable.dtr-column > tbody > tr > th.control:before { + display: inline-block; + color: rgba(0, 0, 0, 0.5); + content: "►"; +} +table.dataTable.dtr-column > tbody > tr > td.dtr-control.arrow-right::before, +table.dataTable.dtr-column > tbody > tr > th.dtr-control.arrow-right::before, +table.dataTable.dtr-column > tbody > tr > td.control.arrow-right::before, +table.dataTable.dtr-column > tbody > tr > th.control.arrow-right::before { + content: "◄"; +} +table.dataTable.dtr-column > tbody > tr.parent td.dtr-control:before, +table.dataTable.dtr-column > tbody > tr.parent th.dtr-control:before, +table.dataTable.dtr-column > tbody > tr.parent td.control:before, +table.dataTable.dtr-column > tbody > tr.parent th.control:before { + content: "▼"; +} +table.dataTable > tbody > tr.child { + padding: 0.5em 1em; +} +table.dataTable > tbody > tr.child:hover { + background: transparent !important; +} +table.dataTable > tbody > tr.child ul.dtr-details { + display: inline-block; + list-style-type: none; + margin: 0; + padding: 0; +} +table.dataTable > tbody > tr.child ul.dtr-details > li { + border-bottom: 1px solid #efefef; + padding: 0.5em 0; +} +table.dataTable > tbody > tr.child ul.dtr-details > li:first-child { + padding-top: 0; +} +table.dataTable > tbody > tr.child ul.dtr-details > li:last-child { + padding-bottom: 0; + border-bottom: none; +} +table.dataTable > tbody > tr.child span.dtr-title { + display: inline-block; + min-width: 75px; + font-weight: bold; +} + +div.dtr-modal { + position: fixed; box-sizing: border-box; - display: block; - content: " "; - border: 2px solid rgb(40, 40, 40); - border-radius: 50%; - border-left-color: transparent; - border-right-color: transparent; - animation: dtb-spinner 1500ms infinite linear; - -o-animation: dtb-spinner 1500ms infinite linear; - -ms-animation: dtb-spinner 1500ms infinite linear; - -webkit-animation: dtb-spinner 1500ms infinite linear; - -moz-animation: dtb-spinner 1500ms infinite linear; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 100; + padding: 10em 1em; } -div.dt-buttons div.btn-group { - position: initial; +div.dtr-modal div.dtr-modal-display { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + width: 50%; + height: fit-content; + max-height: 75%; + overflow: auto; + margin: auto; + z-index: 102; + overflow: auto; + background-color: #f5f5f7; + border: 1px solid black; + border-radius: 0.5em; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.6); +} +div.dtr-modal div.dtr-modal-content { + position: relative; + padding: 2.5em; +} +div.dtr-modal div.dtr-modal-content h2 { + margin-top: 0; +} +div.dtr-modal div.dtr-modal-close { + position: absolute; + top: 6px; + right: 6px; + width: 22px; + height: 22px; + text-align: center; + border-radius: 3px; + cursor: pointer; + z-index: 12; +} +div.dtr-modal div.dtr-modal-background { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 101; + background: rgba(0, 0, 0, 0.6); } -div.dt-btn-split-wrapper:active:not(.disabled) button, div.dt-btn-split-wrapper.active:not(.disabled) button { - background-color: #5a6268; - border-color: #545b62; +@media screen and (max-width: 767px) { + div.dtr-modal div.dtr-modal-display { + width: 95%; + } } -div.dt-btn-split-wrapper:active:not(.disabled) button.dt-btn-split-drop, div.dt-btn-split-wrapper.active:not(.disabled) button.dt-btn-split-drop { - box-shadow: none; - background-color: #6c757d; - border-color: #6c757d; +html.dark table.dataTable > tbody > tr > td.dtr-control:before { + color: rgba(255, 255, 255, 0.5) !important; } -div.dt-btn-split-wrapper:active:not(.disabled) button:hover, div.dt-btn-split-wrapper.active:not(.disabled) button:hover { - background-color: #5a6268; - border-color: #545b62; +html.dark table.dataTable > tbody > tr.child ul.dtr-details > li { + border-bottom-color: rgb(64, 67, 70); +} +html.dark div.dtr-modal div.dtr-modal-display { + background-color: rgb(33, 37, 41); + border: 1px solid rgba(255, 255, 255, 0.15); } -div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group { - border-radius: 4px !important; -} -div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child { - border-top-left-radius: 0px !important; - border-bottom-left-radius: 0px !important; -} -div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:first-child { - border-top-right-radius: 0px !important; - border-bottom-right-radius: 0px !important; -} -div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group:last-child:first-child { - border-top-left-radius: 4px !important; - border-bottom-left-radius: 4px !important; - border-top-right-radius: 4px !important; - border-bottom-right-radius: 4px !important; -} -div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group button.dt-btn-split-drop:last-child { - border: 1px solid rgb(108, 117, 125); -} -div.dataTables_wrapper div.dt-buttons.btn-group div.btn-group div.dt-btn-split-wrapper { - border: none; +div.dtr-bs-modal table.table tr:first-child td { + border-top: none; } -div.dt-button-collection div.btn-group { - border-radius: 4px !important; -} -div.dt-button-collection div.btn-group button { - border-radius: 4px; -} -div.dt-button-collection div.btn-group button:last-child { - border-top-left-radius: 0px !important; - border-bottom-left-radius: 0px !important; -} -div.dt-button-collection div.btn-group button:first-child { - border-top-right-radius: 0px !important; - border-bottom-right-radius: 0px !important; -} -div.dt-button-collection div.btn-group button:last-child:first-child { - border-top-left-radius: 4px !important; - border-bottom-left-radius: 4px !important; - border-top-right-radius: 4px !important; - border-bottom-right-radius: 4px !important; -} -div.dt-button-collection div.btn-group button.dt-btn-split-drop:last-child { - border: 1px solid rgb(108, 117, 125); -} -div.dt-button-collection div.btn-group div.dt-btn-split-wrapper { - border: none; -} - -span.dt-button-spacer.bar:empty { - height: inherit; -} - -div.dt-button-collection span.dt-button-spacer { - padding-left: 1rem !important; - text-align: left; +table.dataTable.table-bordered th.dtr-control.dtr-hidden + *, +table.dataTable.table-bordered td.dtr-control.dtr-hidden + * { + border-left-width: 1px; } diff --git a/public/datatables/datatables.js b/public/datatables/datatables.js index dd11cf7c7..e2187999d 100644 --- a/public/datatables/datatables.js +++ b/public/datatables/datatables.js @@ -4,68 +4,106 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs4/jszip-2.5.0/pdfmake-0.1.36/dt-1.13.2/b-2.3.4/b-html5-2.3.4 + * https://datatables.net/download/#bs5/jszip-3.10.1/pdfmake-0.2.7/dt-1.13.8/b-2.4.2/b-html5-2.4.2/b-print-2.4.2/r-2.5.0 * * Included libraries: - * JSZip 2.5.0, pdfmake 0.1.36, DataTables 1.13.2, Buttons 2.3.4, HTML5 export 2.3.4 + * JSZip 3.10.1, pdfmake 0.2.7, DataTables 1.13.8, Buttons 2.4.2, HTML5 export 2.4.2, Print view 2.4.2, Responsive 2.5.0 */ /*! -JSZip - A Javascript class for generating and reading zip files +JSZip v3.10.1 - A JavaScript class for generating and reading zip files -(c) 2009-2014 Stuart Knightley -Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown. +(c) 2009-2016 Stuart Knightley +Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/main/LICENSE.markdown. JSZip uses the library pako released under the MIT license : -https://github.com/nodeca/pako/blob/master/LICENSE +https://github.com/nodeca/pako/blob/main/LICENSE */ -!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.JSZip=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); - enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); - enc4 = chr3 & 63; + enc3 = remainingBytes > 1 ? (((chr2 & 15) << 2) | (chr3 >> 6)) : 64; + enc4 = remainingBytes > 2 ? (chr3 & 63) : 64; - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } - else if (isNaN(chr3)) { - enc4 = 64; - } - - output = output + _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4); + output.push(_keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4)); } - return output; + return output.join(""); }; // public method for decoding -exports.decode = function(input, utf8) { - var output = ""; +exports.decode = function(input) { var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; - var i = 0; + var i = 0, resultIndex = 0; - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); + var dataUrlPrefix = "data:"; + + if (input.substr(0, dataUrlPrefix.length) === dataUrlPrefix) { + // This is a common error: people give a data url + // (data:image/png;base64,iVBOR...) with a {base64: true} and + // wonders why things don't work. + // We can detect that the string input looks like a data url but we + // *can't* be sure it is one: removing everything up to the comma would + // be too dangerous. + throw new Error("Invalid base64 input, it looks like a data url."); + } + + input = input.replace(/[^A-Za-z0-9+/=]/g, ""); + + var totalLength = input.length * 3 / 4; + if(input.charAt(input.length - 1) === _keyStr.charAt(64)) { + totalLength--; + } + if(input.charAt(input.length - 2) === _keyStr.charAt(64)) { + totalLength--; + } + if (totalLength % 1 !== 0) { + // totalLength is not an integer, the length does not match a valid + // base64 content. That can happen if: + // - the input is not a base64 content + // - the input is *almost* a base64 content, with a extra chars at the + // beginning or at the end + // - the input uses a base64 variant (base64url for example) + throw new Error("Invalid base64 input, bad content length."); + } + var output; + if (support.uint8array) { + output = new Uint8Array(totalLength|0); + } else { + output = new Array(totalLength|0); + } while (i < input.length) { @@ -78,178 +116,1703 @@ exports.decode = function(input, utf8) { chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; - output = output + String.fromCharCode(chr1); + output[resultIndex++] = chr1; - if (enc3 != 64) { - output = output + String.fromCharCode(chr2); + if (enc3 !== 64) { + output[resultIndex++] = chr2; } - if (enc4 != 64) { - output = output + String.fromCharCode(chr3); + if (enc4 !== 64) { + output[resultIndex++] = chr3; } } return output; - }; -},{}],2:[function(_dereq_,module,exports){ -'use strict'; -function CompressedObject() { - this.compressedSize = 0; - this.uncompressedSize = 0; - this.crc32 = 0; - this.compressionMethod = null; - this.compressedContent = null; +},{"./support":30,"./utils":32}],2:[function(require,module,exports){ +"use strict"; + +var external = require("./external"); +var DataWorker = require("./stream/DataWorker"); +var Crc32Probe = require("./stream/Crc32Probe"); +var DataLengthProbe = require("./stream/DataLengthProbe"); + +/** + * Represent a compressed object, with everything needed to decompress it. + * @constructor + * @param {number} compressedSize the size of the data compressed. + * @param {number} uncompressedSize the size of the data after decompression. + * @param {number} crc32 the crc32 of the decompressed file. + * @param {object} compression the type of compression, see lib/compressions.js. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the compressed data. + */ +function CompressedObject(compressedSize, uncompressedSize, crc32, compression, data) { + this.compressedSize = compressedSize; + this.uncompressedSize = uncompressedSize; + this.crc32 = crc32; + this.compression = compression; + this.compressedContent = data; } CompressedObject.prototype = { /** - * Return the decompressed content in an unspecified format. - * The format will depend on the decompressor. - * @return {Object} the decompressed content. + * Create a worker to get the uncompressed content. + * @return {GenericWorker} the worker. */ - getContent: function() { - return null; // see implementation + getContentWorker: function () { + var worker = new DataWorker(external.Promise.resolve(this.compressedContent)) + .pipe(this.compression.uncompressWorker()) + .pipe(new DataLengthProbe("data_length")); + + var that = this; + worker.on("end", function () { + if (this.streamInfo["data_length"] !== that.uncompressedSize) { + throw new Error("Bug : uncompressed data size mismatch"); + } + }); + return worker; }, /** - * Return the compressed content in an unspecified format. - * The format will depend on the compressed conten source. - * @return {Object} the compressed content. + * Create a worker to get the compressed content. + * @return {GenericWorker} the worker. */ - getCompressedContent: function() { - return null; // see implementation + getCompressedWorker: function () { + return new DataWorker(external.Promise.resolve(this.compressedContent)) + .withStreamInfo("compressedSize", this.compressedSize) + .withStreamInfo("uncompressedSize", this.uncompressedSize) + .withStreamInfo("crc32", this.crc32) + .withStreamInfo("compression", this.compression) + ; } }; -module.exports = CompressedObject; - -},{}],3:[function(_dereq_,module,exports){ -'use strict'; -exports.STORE = { - magic: "\x00\x00", - compress: function(content, compressionOptions) { - return content; // no compression - }, - uncompress: function(content) { - return content; // no compression - }, - compressInputType: null, - uncompressInputType: null -}; -exports.DEFLATE = _dereq_('./flate'); - -},{"./flate":8}],4:[function(_dereq_,module,exports){ -'use strict'; - -var utils = _dereq_('./utils'); - -var table = [ - 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, - 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, - 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, - 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, - 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, - 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, - 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, - 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, - 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, - 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, - 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, - 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, - 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, - 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, - 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, - 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, - 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, - 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, - 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, - 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, - 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, - 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, - 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, - 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, - 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, - 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, - 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, - 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, - 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, - 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, - 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, - 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, - 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, - 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, - 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, - 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, - 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, - 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, - 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, - 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, - 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, - 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, - 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, - 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, - 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, - 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, - 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, - 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, - 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, - 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, - 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, - 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, - 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, - 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, - 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, - 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, - 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, - 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, - 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, - 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, - 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, - 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, - 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, - 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D -]; /** - * - * Javascript crc32 - * http://www.webtoolkit.info/ - * + * Chain the given worker with other workers to compress the content with the + * given compression. + * @param {GenericWorker} uncompressedWorker the worker to pipe. + * @param {Object} compression the compression object. + * @param {Object} compressionOptions the options to use when compressing. + * @return {GenericWorker} the new worker compressing the content. */ -module.exports = function crc32(input, crc) { +CompressedObject.createWorkerFrom = function (uncompressedWorker, compression, compressionOptions) { + return uncompressedWorker + .pipe(new Crc32Probe()) + .pipe(new DataLengthProbe("uncompressedSize")) + .pipe(compression.compressWorker(compressionOptions)) + .pipe(new DataLengthProbe("compressedSize")) + .withStreamInfo("compression", compression); +}; + +module.exports = CompressedObject; + +},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(require,module,exports){ +"use strict"; + +var GenericWorker = require("./stream/GenericWorker"); + +exports.STORE = { + magic: "\x00\x00", + compressWorker : function () { + return new GenericWorker("STORE compression"); + }, + uncompressWorker : function () { + return new GenericWorker("STORE decompression"); + } +}; +exports.DEFLATE = require("./flate"); + +},{"./flate":7,"./stream/GenericWorker":28}],4:[function(require,module,exports){ +"use strict"; + +var utils = require("./utils"); + +/** + * The following functions come from pako, from pako/lib/zlib/crc32.js + * released under the MIT license, see pako https://github.com/nodeca/pako/ + */ + +// Use ordinary array, since untyped makes no boost here +function makeTable() { + var c, table = []; + + for(var n =0; n < 256; n++){ + c = n; + for(var k =0; k < 8; k++){ + c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)); + } + table[n] = c; + } + + return table; +} + +// Create table on load. Just 255 signed longs. Not a problem. +var crcTable = makeTable(); + + +function crc32(crc, buf, len, pos) { + var t = crcTable, end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++ ) { + crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + +// That's all for the pako functions. + +/** + * Compute the crc32 of a string. + * This is almost the same as the function crc32, but for strings. Using the + * same function for the two use cases leads to horrible performances. + * @param {Number} crc the starting value of the crc. + * @param {String} str the string to use. + * @param {Number} len the length of the string. + * @param {Number} pos the starting position for the crc32 computation. + * @return {Number} the computed crc32. + */ +function crc32str(crc, str, len, pos) { + var t = crcTable, end = pos + len; + + crc = crc ^ (-1); + + for (var i = pos; i < end; i++ ) { + crc = (crc >>> 8) ^ t[(crc ^ str.charCodeAt(i)) & 0xFF]; + } + + return (crc ^ (-1)); // >>> 0; +} + +module.exports = function crc32wrapper(input, crc) { if (typeof input === "undefined" || !input.length) { return 0; } var isArray = utils.getTypeOf(input) !== "string"; - if (typeof(crc) == "undefined") { - crc = 0; + if(isArray) { + return crc32(crc|0, input, input.length, 0); + } else { + return crc32str(crc|0, input, input.length, 0); } - var x = 0; - var y = 0; - var b = 0; - - crc = crc ^ (-1); - for (var i = 0, iTop = input.length; i < iTop; i++) { - b = isArray ? input[i] : input.charCodeAt(i); - y = (crc ^ b) & 0xFF; - x = table[y]; - crc = (crc >>> 8) ^ x; - } - - return crc ^ (-1); }; -// vim: set shiftwidth=4 softtabstop=4: -},{"./utils":21}],5:[function(_dereq_,module,exports){ -'use strict'; -var utils = _dereq_('./utils'); +},{"./utils":32}],5:[function(require,module,exports){ +"use strict"; +exports.base64 = false; +exports.binary = false; +exports.dir = false; +exports.createFolders = true; +exports.date = null; +exports.compression = null; +exports.compressionOptions = null; +exports.comment = null; +exports.unixPermissions = null; +exports.dosPermissions = null; + +},{}],6:[function(require,module,exports){ +"use strict"; + +// load the global object first: +// - it should be better integrated in the system (unhandledRejection in node) +// - the environment may have a custom Promise implementation (see zone.js) +var ES6Promise = null; +if (typeof Promise !== "undefined") { + ES6Promise = Promise; +} else { + ES6Promise = require("lie"); +} + +/** + * Let the user use/change some implementations. + */ +module.exports = { + Promise: ES6Promise +}; + +},{"lie":37}],7:[function(require,module,exports){ +"use strict"; +var USE_TYPEDARRAY = (typeof Uint8Array !== "undefined") && (typeof Uint16Array !== "undefined") && (typeof Uint32Array !== "undefined"); + +var pako = require("pako"); +var utils = require("./utils"); +var GenericWorker = require("./stream/GenericWorker"); + +var ARRAY_TYPE = USE_TYPEDARRAY ? "uint8array" : "array"; + +exports.magic = "\x08\x00"; + +/** + * Create a worker that uses pako to inflate/deflate. + * @constructor + * @param {String} action the name of the pako function to call : either "Deflate" or "Inflate". + * @param {Object} options the options to use when (de)compressing. + */ +function FlateWorker(action, options) { + GenericWorker.call(this, "FlateWorker/" + action); + + this._pako = null; + this._pakoAction = action; + this._pakoOptions = options; + // the `meta` object from the last chunk received + // this allow this worker to pass around metadata + this.meta = {}; +} + +utils.inherits(FlateWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +FlateWorker.prototype.processChunk = function (chunk) { + this.meta = chunk.meta; + if (this._pako === null) { + this._createPako(); + } + this._pako.push(utils.transformTo(ARRAY_TYPE, chunk.data), false); +}; + +/** + * @see GenericWorker.flush + */ +FlateWorker.prototype.flush = function () { + GenericWorker.prototype.flush.call(this); + if (this._pako === null) { + this._createPako(); + } + this._pako.push([], true); +}; +/** + * @see GenericWorker.cleanUp + */ +FlateWorker.prototype.cleanUp = function () { + GenericWorker.prototype.cleanUp.call(this); + this._pako = null; +}; + +/** + * Create the _pako object. + * TODO: lazy-loading this object isn't the best solution but it's the + * quickest. The best solution is to lazy-load the worker list. See also the + * issue #446. + */ +FlateWorker.prototype._createPako = function () { + this._pako = new pako[this._pakoAction]({ + raw: true, + level: this._pakoOptions.level || -1 // default compression + }); + var self = this; + this._pako.onData = function(data) { + self.push({ + data : data, + meta : self.meta + }); + }; +}; + +exports.compressWorker = function (compressionOptions) { + return new FlateWorker("Deflate", compressionOptions); +}; +exports.uncompressWorker = function () { + return new FlateWorker("Inflate", {}); +}; + +},{"./stream/GenericWorker":28,"./utils":32,"pako":38}],8:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("../stream/GenericWorker"); +var utf8 = require("../utf8"); +var crc32 = require("../crc32"); +var signature = require("../signature"); + +/** + * Transform an integer into a string in hexadecimal. + * @private + * @param {number} dec the number to convert. + * @param {number} bytes the number of bytes to generate. + * @returns {string} the result. + */ +var decToHex = function(dec, bytes) { + var hex = "", i; + for (i = 0; i < bytes; i++) { + hex += String.fromCharCode(dec & 0xff); + dec = dec >>> 8; + } + return hex; +}; + +/** + * Generate the UNIX part of the external file attributes. + * @param {Object} unixPermissions the unix permissions or null. + * @param {Boolean} isDir true if the entry is a directory, false otherwise. + * @return {Number} a 32 bit integer. + * + * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute : + * + * TTTTsstrwxrwxrwx0000000000ADVSHR + * ^^^^____________________________ file type, see zipinfo.c (UNX_*) + * ^^^_________________________ setuid, setgid, sticky + * ^^^^^^^^^________________ permissions + * ^^^^^^^^^^______ not used ? + * ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only + */ +var generateUnixExternalFileAttr = function (unixPermissions, isDir) { + + var result = unixPermissions; + if (!unixPermissions) { + // I can't use octal values in strict mode, hence the hexa. + // 040775 => 0x41fd + // 0100664 => 0x81b4 + result = isDir ? 0x41fd : 0x81b4; + } + return (result & 0xFFFF) << 16; +}; + +/** + * Generate the DOS part of the external file attributes. + * @param {Object} dosPermissions the dos permissions or null. + * @param {Boolean} isDir true if the entry is a directory, false otherwise. + * @return {Number} a 32 bit integer. + * + * Bit 0 Read-Only + * Bit 1 Hidden + * Bit 2 System + * Bit 3 Volume Label + * Bit 4 Directory + * Bit 5 Archive + */ +var generateDosExternalFileAttr = function (dosPermissions) { + // the dir flag is already set for compatibility + return (dosPermissions || 0) & 0x3F; +}; + +/** + * Generate the various parts used in the construction of the final zip file. + * @param {Object} streamInfo the hash with information about the compressed file. + * @param {Boolean} streamedContent is the content streamed ? + * @param {Boolean} streamingEnded is the stream finished ? + * @param {number} offset the current offset from the start of the zip file. + * @param {String} platform let's pretend we are this platform (change platform dependents fields) + * @param {Function} encodeFileName the function to encode the file name / comment. + * @return {Object} the zip parts. + */ +var generateZipParts = function(streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) { + var file = streamInfo["file"], + compression = streamInfo["compression"], + useCustomEncoding = encodeFileName !== utf8.utf8encode, + encodedFileName = utils.transformTo("string", encodeFileName(file.name)), + utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)), + comment = file.comment, + encodedComment = utils.transformTo("string", encodeFileName(comment)), + utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)), + useUTF8ForFileName = utfEncodedFileName.length !== file.name.length, + useUTF8ForComment = utfEncodedComment.length !== comment.length, + dosTime, + dosDate, + extraFields = "", + unicodePathExtraField = "", + unicodeCommentExtraField = "", + dir = file.dir, + date = file.date; + + + var dataInfo = { + crc32 : 0, + compressedSize : 0, + uncompressedSize : 0 + }; + + // if the content is streamed, the sizes/crc32 are only available AFTER + // the end of the stream. + if (!streamedContent || streamingEnded) { + dataInfo.crc32 = streamInfo["crc32"]; + dataInfo.compressedSize = streamInfo["compressedSize"]; + dataInfo.uncompressedSize = streamInfo["uncompressedSize"]; + } + + var bitflag = 0; + if (streamedContent) { + // Bit 3: the sizes/crc32 are set to zero in the local header. + // The correct values are put in the data descriptor immediately + // following the compressed data. + bitflag |= 0x0008; + } + if (!useCustomEncoding && (useUTF8ForFileName || useUTF8ForComment)) { + // Bit 11: Language encoding flag (EFS). + bitflag |= 0x0800; + } + + + var extFileAttr = 0; + var versionMadeBy = 0; + if (dir) { + // dos or unix, we set the dos dir flag + extFileAttr |= 0x00010; + } + if(platform === "UNIX") { + versionMadeBy = 0x031E; // UNIX, version 3.0 + extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir); + } else { // DOS or other, fallback to DOS + versionMadeBy = 0x0014; // DOS, version 2.0 + extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir); + } + + // date + // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html + // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html + // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html + + dosTime = date.getUTCHours(); + dosTime = dosTime << 6; + dosTime = dosTime | date.getUTCMinutes(); + dosTime = dosTime << 5; + dosTime = dosTime | date.getUTCSeconds() / 2; + + dosDate = date.getUTCFullYear() - 1980; + dosDate = dosDate << 4; + dosDate = dosDate | (date.getUTCMonth() + 1); + dosDate = dosDate << 5; + dosDate = dosDate | date.getUTCDate(); + + if (useUTF8ForFileName) { + // set the unicode path extra field. unzip needs at least one extra + // field to correctly handle unicode path, so using the path is as good + // as any other information. This could improve the situation with + // other archive managers too. + // This field is usually used without the utf8 flag, with a non + // unicode path in the header (winrar, winzip). This helps (a bit) + // with the messy Windows' default compressed folders feature but + // breaks on p7zip which doesn't seek the unicode path extra field. + // So for now, UTF-8 everywhere ! + unicodePathExtraField = + // Version + decToHex(1, 1) + + // NameCRC32 + decToHex(crc32(encodedFileName), 4) + + // UnicodeName + utfEncodedFileName; + + extraFields += + // Info-ZIP Unicode Path Extra Field + "\x75\x70" + + // size + decToHex(unicodePathExtraField.length, 2) + + // content + unicodePathExtraField; + } + + if(useUTF8ForComment) { + + unicodeCommentExtraField = + // Version + decToHex(1, 1) + + // CommentCRC32 + decToHex(crc32(encodedComment), 4) + + // UnicodeName + utfEncodedComment; + + extraFields += + // Info-ZIP Unicode Path Extra Field + "\x75\x63" + + // size + decToHex(unicodeCommentExtraField.length, 2) + + // content + unicodeCommentExtraField; + } + + var header = ""; + + // version needed to extract + header += "\x0A\x00"; + // general purpose bit flag + header += decToHex(bitflag, 2); + // compression method + header += compression.magic; + // last mod file time + header += decToHex(dosTime, 2); + // last mod file date + header += decToHex(dosDate, 2); + // crc-32 + header += decToHex(dataInfo.crc32, 4); + // compressed size + header += decToHex(dataInfo.compressedSize, 4); + // uncompressed size + header += decToHex(dataInfo.uncompressedSize, 4); + // file name length + header += decToHex(encodedFileName.length, 2); + // extra field length + header += decToHex(extraFields.length, 2); + + + var fileRecord = signature.LOCAL_FILE_HEADER + header + encodedFileName + extraFields; + + var dirRecord = signature.CENTRAL_FILE_HEADER + + // version made by (00: DOS) + decToHex(versionMadeBy, 2) + + // file header (common to file and central directory) + header + + // file comment length + decToHex(encodedComment.length, 2) + + // disk number start + "\x00\x00" + + // internal file attributes TODO + "\x00\x00" + + // external file attributes + decToHex(extFileAttr, 4) + + // relative offset of local header + decToHex(offset, 4) + + // file name + encodedFileName + + // extra field + extraFields + + // file comment + encodedComment; + + return { + fileRecord: fileRecord, + dirRecord: dirRecord + }; +}; + +/** + * Generate the EOCD record. + * @param {Number} entriesCount the number of entries in the zip file. + * @param {Number} centralDirLength the length (in bytes) of the central dir. + * @param {Number} localDirLength the length (in bytes) of the local dir. + * @param {String} comment the zip file comment as a binary string. + * @param {Function} encodeFileName the function to encode the comment. + * @return {String} the EOCD record. + */ +var generateCentralDirectoryEnd = function (entriesCount, centralDirLength, localDirLength, comment, encodeFileName) { + var dirEnd = ""; + var encodedComment = utils.transformTo("string", encodeFileName(comment)); + + // end of central dir signature + dirEnd = signature.CENTRAL_DIRECTORY_END + + // number of this disk + "\x00\x00" + + // number of the disk with the start of the central directory + "\x00\x00" + + // total number of entries in the central directory on this disk + decToHex(entriesCount, 2) + + // total number of entries in the central directory + decToHex(entriesCount, 2) + + // size of the central directory 4 bytes + decToHex(centralDirLength, 4) + + // offset of start of central directory with respect to the starting disk number + decToHex(localDirLength, 4) + + // .ZIP file comment length + decToHex(encodedComment.length, 2) + + // .ZIP file comment + encodedComment; + + return dirEnd; +}; + +/** + * Generate data descriptors for a file entry. + * @param {Object} streamInfo the hash generated by a worker, containing information + * on the file entry. + * @return {String} the data descriptors. + */ +var generateDataDescriptors = function (streamInfo) { + var descriptor = ""; + descriptor = signature.DATA_DESCRIPTOR + + // crc-32 4 bytes + decToHex(streamInfo["crc32"], 4) + + // compressed size 4 bytes + decToHex(streamInfo["compressedSize"], 4) + + // uncompressed size 4 bytes + decToHex(streamInfo["uncompressedSize"], 4); + + return descriptor; +}; + + +/** + * A worker to concatenate other workers to create a zip file. + * @param {Boolean} streamFiles `true` to stream the content of the files, + * `false` to accumulate it. + * @param {String} comment the comment to use. + * @param {String} platform the platform to use, "UNIX" or "DOS". + * @param {Function} encodeFileName the function to encode file names and comments. + */ +function ZipFileWorker(streamFiles, comment, platform, encodeFileName) { + GenericWorker.call(this, "ZipFileWorker"); + // The number of bytes written so far. This doesn't count accumulated chunks. + this.bytesWritten = 0; + // The comment of the zip file + this.zipComment = comment; + // The platform "generating" the zip file. + this.zipPlatform = platform; + // the function to encode file names and comments. + this.encodeFileName = encodeFileName; + // Should we stream the content of the files ? + this.streamFiles = streamFiles; + // If `streamFiles` is false, we will need to accumulate the content of the + // files to calculate sizes / crc32 (and write them *before* the content). + // This boolean indicates if we are accumulating chunks (it will change a lot + // during the lifetime of this worker). + this.accumulate = false; + // The buffer receiving chunks when accumulating content. + this.contentBuffer = []; + // The list of generated directory records. + this.dirRecords = []; + // The offset (in bytes) from the beginning of the zip file for the current source. + this.currentSourceOffset = 0; + // The total number of entries in this zip file. + this.entriesCount = 0; + // the name of the file currently being added, null when handling the end of the zip file. + // Used for the emitted metadata. + this.currentFile = null; + + + + this._sources = []; +} +utils.inherits(ZipFileWorker, GenericWorker); + +/** + * @see GenericWorker.push + */ +ZipFileWorker.prototype.push = function (chunk) { + + var currentFilePercent = chunk.meta.percent || 0; + var entriesCount = this.entriesCount; + var remainingFiles = this._sources.length; + + if(this.accumulate) { + this.contentBuffer.push(chunk); + } else { + this.bytesWritten += chunk.data.length; + + GenericWorker.prototype.push.call(this, { + data : chunk.data, + meta : { + currentFile : this.currentFile, + percent : entriesCount ? (currentFilePercent + 100 * (entriesCount - remainingFiles - 1)) / entriesCount : 100 + } + }); + } +}; + +/** + * The worker started a new source (an other worker). + * @param {Object} streamInfo the streamInfo object from the new source. + */ +ZipFileWorker.prototype.openedSource = function (streamInfo) { + this.currentSourceOffset = this.bytesWritten; + this.currentFile = streamInfo["file"].name; + + var streamedContent = this.streamFiles && !streamInfo["file"].dir; + + // don't stream folders (because they don't have any content) + if(streamedContent) { + var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName); + this.push({ + data : record.fileRecord, + meta : {percent:0} + }); + } else { + // we need to wait for the whole file before pushing anything + this.accumulate = true; + } +}; + +/** + * The worker finished a source (an other worker). + * @param {Object} streamInfo the streamInfo object from the finished source. + */ +ZipFileWorker.prototype.closedSource = function (streamInfo) { + this.accumulate = false; + var streamedContent = this.streamFiles && !streamInfo["file"].dir; + var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName); + + this.dirRecords.push(record.dirRecord); + if(streamedContent) { + // after the streamed file, we put data descriptors + this.push({ + data : generateDataDescriptors(streamInfo), + meta : {percent:100} + }); + } else { + // the content wasn't streamed, we need to push everything now + // first the file record, then the content + this.push({ + data : record.fileRecord, + meta : {percent:0} + }); + while(this.contentBuffer.length) { + this.push(this.contentBuffer.shift()); + } + } + this.currentFile = null; +}; + +/** + * @see GenericWorker.flush + */ +ZipFileWorker.prototype.flush = function () { + + var localDirLength = this.bytesWritten; + for(var i = 0; i < this.dirRecords.length; i++) { + this.push({ + data : this.dirRecords[i], + meta : {percent:100} + }); + } + var centralDirLength = this.bytesWritten - localDirLength; + + var dirEnd = generateCentralDirectoryEnd(this.dirRecords.length, centralDirLength, localDirLength, this.zipComment, this.encodeFileName); + + this.push({ + data : dirEnd, + meta : {percent:100} + }); +}; + +/** + * Prepare the next source to be read. + */ +ZipFileWorker.prototype.prepareNextSource = function () { + this.previous = this._sources.shift(); + this.openedSource(this.previous.streamInfo); + if (this.isPaused) { + this.previous.pause(); + } else { + this.previous.resume(); + } +}; + +/** + * @see GenericWorker.registerPrevious + */ +ZipFileWorker.prototype.registerPrevious = function (previous) { + this._sources.push(previous); + var self = this; + + previous.on("data", function (chunk) { + self.processChunk(chunk); + }); + previous.on("end", function () { + self.closedSource(self.previous.streamInfo); + if(self._sources.length) { + self.prepareNextSource(); + } else { + self.end(); + } + }); + previous.on("error", function (e) { + self.error(e); + }); + return this; +}; + +/** + * @see GenericWorker.resume + */ +ZipFileWorker.prototype.resume = function () { + if(!GenericWorker.prototype.resume.call(this)) { + return false; + } + + if (!this.previous && this._sources.length) { + this.prepareNextSource(); + return true; + } + if (!this.previous && !this._sources.length && !this.generatedError) { + this.end(); + return true; + } +}; + +/** + * @see GenericWorker.error + */ +ZipFileWorker.prototype.error = function (e) { + var sources = this._sources; + if(!GenericWorker.prototype.error.call(this, e)) { + return false; + } + for(var i = 0; i < sources.length; i++) { + try { + sources[i].error(e); + } catch(e) { + // the `error` exploded, nothing to do + } + } + return true; +}; + +/** + * @see GenericWorker.lock + */ +ZipFileWorker.prototype.lock = function () { + GenericWorker.prototype.lock.call(this); + var sources = this._sources; + for(var i = 0; i < sources.length; i++) { + sources[i].lock(); + } +}; + +module.exports = ZipFileWorker; + +},{"../crc32":4,"../signature":23,"../stream/GenericWorker":28,"../utf8":31,"../utils":32}],9:[function(require,module,exports){ +"use strict"; + +var compressions = require("../compressions"); +var ZipFileWorker = require("./ZipFileWorker"); + +/** + * Find the compression to use. + * @param {String} fileCompression the compression defined at the file level, if any. + * @param {String} zipCompression the compression defined at the load() level. + * @return {Object} the compression object to use. + */ +var getCompression = function (fileCompression, zipCompression) { + + var compressionName = fileCompression || zipCompression; + var compression = compressions[compressionName]; + if (!compression) { + throw new Error(compressionName + " is not a valid compression method !"); + } + return compression; +}; + +/** + * Create a worker to generate a zip file. + * @param {JSZip} zip the JSZip instance at the right root level. + * @param {Object} options to generate the zip file. + * @param {String} comment the comment to use. + */ +exports.generateWorker = function (zip, options, comment) { + + var zipFileWorker = new ZipFileWorker(options.streamFiles, comment, options.platform, options.encodeFileName); + var entriesCount = 0; + try { + + zip.forEach(function (relativePath, file) { + entriesCount++; + var compression = getCompression(file.options.compression, options.compression); + var compressionOptions = file.options.compressionOptions || options.compressionOptions || {}; + var dir = file.dir, date = file.date; + + file._compressWorker(compression, compressionOptions) + .withStreamInfo("file", { + name : relativePath, + dir : dir, + date : date, + comment : file.comment || "", + unixPermissions : file.unixPermissions, + dosPermissions : file.dosPermissions + }) + .pipe(zipFileWorker); + }); + zipFileWorker.entriesCount = entriesCount; + } catch (e) { + zipFileWorker.error(e); + } + + return zipFileWorker; +}; + +},{"../compressions":3,"./ZipFileWorker":8}],10:[function(require,module,exports){ +"use strict"; + +/** + * Representation a of zip file in js + * @constructor + */ +function JSZip() { + // if this constructor is used without `new`, it adds `new` before itself: + if(!(this instanceof JSZip)) { + return new JSZip(); + } + + if(arguments.length) { + throw new Error("The constructor with parameters has been removed in JSZip 3.0, please check the upgrade guide."); + } + + // object containing the files : + // { + // "folder/" : {...}, + // "folder/data.txt" : {...} + // } + // NOTE: we use a null prototype because we do not + // want filenames like "toString" coming from a zip file + // to overwrite methods and attributes in a normal Object. + this.files = Object.create(null); + + this.comment = null; + + // Where we are in the hierarchy + this.root = ""; + this.clone = function() { + var newObj = new JSZip(); + for (var i in this) { + if (typeof this[i] !== "function") { + newObj[i] = this[i]; + } + } + return newObj; + }; +} +JSZip.prototype = require("./object"); +JSZip.prototype.loadAsync = require("./load"); +JSZip.support = require("./support"); +JSZip.defaults = require("./defaults"); + +// TODO find a better way to handle this version, +// a require('package.json').version doesn't work with webpack, see #327 +JSZip.version = "3.10.1"; + +JSZip.loadAsync = function (content, options) { + return new JSZip().loadAsync(content, options); +}; + +JSZip.external = require("./external"); +module.exports = JSZip; + +},{"./defaults":5,"./external":6,"./load":11,"./object":15,"./support":30}],11:[function(require,module,exports){ +"use strict"; +var utils = require("./utils"); +var external = require("./external"); +var utf8 = require("./utf8"); +var ZipEntries = require("./zipEntries"); +var Crc32Probe = require("./stream/Crc32Probe"); +var nodejsUtils = require("./nodejsUtils"); + +/** + * Check the CRC32 of an entry. + * @param {ZipEntry} zipEntry the zip entry to check. + * @return {Promise} the result. + */ +function checkEntryCRC32(zipEntry) { + return new external.Promise(function (resolve, reject) { + var worker = zipEntry.decompressed.getContentWorker().pipe(new Crc32Probe()); + worker.on("error", function (e) { + reject(e); + }) + .on("end", function () { + if (worker.streamInfo.crc32 !== zipEntry.decompressed.crc32) { + reject(new Error("Corrupted zip : CRC32 mismatch")); + } else { + resolve(); + } + }) + .resume(); + }); +} + +module.exports = function (data, options) { + var zip = this; + options = utils.extend(options || {}, { + base64: false, + checkCRC32: false, + optimizedBinaryString: false, + createFolders: false, + decodeFileName: utf8.utf8decode + }); + + if (nodejsUtils.isNode && nodejsUtils.isStream(data)) { + return external.Promise.reject(new Error("JSZip can't accept a stream when loading a zip file.")); + } + + return utils.prepareContent("the loaded zip file", data, true, options.optimizedBinaryString, options.base64) + .then(function (data) { + var zipEntries = new ZipEntries(options); + zipEntries.load(data); + return zipEntries; + }).then(function checkCRC32(zipEntries) { + var promises = [external.Promise.resolve(zipEntries)]; + var files = zipEntries.files; + if (options.checkCRC32) { + for (var i = 0; i < files.length; i++) { + promises.push(checkEntryCRC32(files[i])); + } + } + return external.Promise.all(promises); + }).then(function addFiles(results) { + var zipEntries = results.shift(); + var files = zipEntries.files; + for (var i = 0; i < files.length; i++) { + var input = files[i]; + + var unsafeName = input.fileNameStr; + var safeName = utils.resolve(input.fileNameStr); + + zip.file(safeName, input.decompressed, { + binary: true, + optimizedBinaryString: true, + date: input.date, + dir: input.dir, + comment: input.fileCommentStr.length ? input.fileCommentStr : null, + unixPermissions: input.unixPermissions, + dosPermissions: input.dosPermissions, + createFolders: options.createFolders + }); + if (!input.dir) { + zip.file(safeName).unsafeOriginalName = unsafeName; + } + } + if (zipEntries.zipComment.length) { + zip.comment = zipEntries.zipComment; + } + + return zip; + }); +}; + +},{"./external":6,"./nodejsUtils":14,"./stream/Crc32Probe":25,"./utf8":31,"./utils":32,"./zipEntries":33}],12:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("../stream/GenericWorker"); + +/** + * A worker that use a nodejs stream as source. + * @constructor + * @param {String} filename the name of the file entry for this stream. + * @param {Readable} stream the nodejs stream. + */ +function NodejsStreamInputAdapter(filename, stream) { + GenericWorker.call(this, "Nodejs stream input adapter for " + filename); + this._upstreamEnded = false; + this._bindStream(stream); +} + +utils.inherits(NodejsStreamInputAdapter, GenericWorker); + +/** + * Prepare the stream and bind the callbacks on it. + * Do this ASAP on node 0.10 ! A lazy binding doesn't always work. + * @param {Stream} stream the nodejs stream to use. + */ +NodejsStreamInputAdapter.prototype._bindStream = function (stream) { + var self = this; + this._stream = stream; + stream.pause(); + stream + .on("data", function (chunk) { + self.push({ + data: chunk, + meta : { + percent : 0 + } + }); + }) + .on("error", function (e) { + if(self.isPaused) { + this.generatedError = e; + } else { + self.error(e); + } + }) + .on("end", function () { + if(self.isPaused) { + self._upstreamEnded = true; + } else { + self.end(); + } + }); +}; +NodejsStreamInputAdapter.prototype.pause = function () { + if(!GenericWorker.prototype.pause.call(this)) { + return false; + } + this._stream.pause(); + return true; +}; +NodejsStreamInputAdapter.prototype.resume = function () { + if(!GenericWorker.prototype.resume.call(this)) { + return false; + } + + if(this._upstreamEnded) { + this.end(); + } else { + this._stream.resume(); + } + + return true; +}; + +module.exports = NodejsStreamInputAdapter; + +},{"../stream/GenericWorker":28,"../utils":32}],13:[function(require,module,exports){ +"use strict"; + +var Readable = require("readable-stream").Readable; + +var utils = require("../utils"); +utils.inherits(NodejsStreamOutputAdapter, Readable); + +/** +* A nodejs stream using a worker as source. +* @see the SourceWrapper in http://nodejs.org/api/stream.html +* @constructor +* @param {StreamHelper} helper the helper wrapping the worker +* @param {Object} options the nodejs stream options +* @param {Function} updateCb the update callback. +*/ +function NodejsStreamOutputAdapter(helper, options, updateCb) { + Readable.call(this, options); + this._helper = helper; + + var self = this; + helper.on("data", function (data, meta) { + if (!self.push(data)) { + self._helper.pause(); + } + if(updateCb) { + updateCb(meta); + } + }) + .on("error", function(e) { + self.emit("error", e); + }) + .on("end", function () { + self.push(null); + }); +} + + +NodejsStreamOutputAdapter.prototype._read = function() { + this._helper.resume(); +}; + +module.exports = NodejsStreamOutputAdapter; + +},{"../utils":32,"readable-stream":16}],14:[function(require,module,exports){ +"use strict"; + +module.exports = { + /** + * True if this is running in Nodejs, will be undefined in a browser. + * In a browser, browserify won't include this file and the whole module + * will be resolved an empty object. + */ + isNode : typeof Buffer !== "undefined", + /** + * Create a new nodejs Buffer from an existing content. + * @param {Object} data the data to pass to the constructor. + * @param {String} encoding the encoding to use. + * @return {Buffer} a new Buffer. + */ + newBufferFrom: function(data, encoding) { + if (Buffer.from && Buffer.from !== Uint8Array.from) { + return Buffer.from(data, encoding); + } else { + if (typeof data === "number") { + // Safeguard for old Node.js versions. On newer versions, + // Buffer.from(number) / Buffer(number, encoding) already throw. + throw new Error("The \"data\" argument must not be a number"); + } + return new Buffer(data, encoding); + } + }, + /** + * Create a new nodejs Buffer with the specified size. + * @param {Integer} size the size of the buffer. + * @return {Buffer} a new Buffer. + */ + allocBuffer: function (size) { + if (Buffer.alloc) { + return Buffer.alloc(size); + } else { + var buf = new Buffer(size); + buf.fill(0); + return buf; + } + }, + /** + * Find out if an object is a Buffer. + * @param {Object} b the object to test. + * @return {Boolean} true if the object is a Buffer, false otherwise. + */ + isBuffer : function(b){ + return Buffer.isBuffer(b); + }, + + isStream : function (obj) { + return obj && + typeof obj.on === "function" && + typeof obj.pause === "function" && + typeof obj.resume === "function"; + } +}; + +},{}],15:[function(require,module,exports){ +"use strict"; +var utf8 = require("./utf8"); +var utils = require("./utils"); +var GenericWorker = require("./stream/GenericWorker"); +var StreamHelper = require("./stream/StreamHelper"); +var defaults = require("./defaults"); +var CompressedObject = require("./compressedObject"); +var ZipObject = require("./zipObject"); +var generate = require("./generate"); +var nodejsUtils = require("./nodejsUtils"); +var NodejsStreamInputAdapter = require("./nodejs/NodejsStreamInputAdapter"); + + +/** + * Add a file in the current folder. + * @private + * @param {string} name the name of the file + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data of the file + * @param {Object} originalOptions the options of the file + * @return {Object} the new file. + */ +var fileAdd = function(name, data, originalOptions) { + // be sure sub folders exist + var dataType = utils.getTypeOf(data), + parent; + + + /* + * Correct options. + */ + + var o = utils.extend(originalOptions || {}, defaults); + o.date = o.date || new Date(); + if (o.compression !== null) { + o.compression = o.compression.toUpperCase(); + } + + if (typeof o.unixPermissions === "string") { + o.unixPermissions = parseInt(o.unixPermissions, 8); + } + + // UNX_IFDIR 0040000 see zipinfo.c + if (o.unixPermissions && (o.unixPermissions & 0x4000)) { + o.dir = true; + } + // Bit 4 Directory + if (o.dosPermissions && (o.dosPermissions & 0x0010)) { + o.dir = true; + } + + if (o.dir) { + name = forceTrailingSlash(name); + } + if (o.createFolders && (parent = parentFolder(name))) { + folderAdd.call(this, parent, true); + } + + var isUnicodeString = dataType === "string" && o.binary === false && o.base64 === false; + if (!originalOptions || typeof originalOptions.binary === "undefined") { + o.binary = !isUnicodeString; + } + + + var isCompressedEmpty = (data instanceof CompressedObject) && data.uncompressedSize === 0; + + if (isCompressedEmpty || o.dir || !data || data.length === 0) { + o.base64 = false; + o.binary = true; + data = ""; + o.compression = "STORE"; + dataType = "string"; + } + + /* + * Convert content to fit. + */ + + var zipObjectContent = null; + if (data instanceof CompressedObject || data instanceof GenericWorker) { + zipObjectContent = data; + } else if (nodejsUtils.isNode && nodejsUtils.isStream(data)) { + zipObjectContent = new NodejsStreamInputAdapter(name, data); + } else { + zipObjectContent = utils.prepareContent(name, data, o.binary, o.optimizedBinaryString, o.base64); + } + + var object = new ZipObject(name, zipObjectContent, o); + this.files[name] = object; + /* + TODO: we can't throw an exception because we have async promises + (we can have a promise of a Date() for example) but returning a + promise is useless because file(name, data) returns the JSZip + object for chaining. Should we break that to allow the user + to catch the error ? + + return external.Promise.resolve(zipObjectContent) + .then(function () { + return object; + }); + */ +}; + +/** + * Find the parent folder of the path. + * @private + * @param {string} path the path to use + * @return {string} the parent folder, or "" + */ +var parentFolder = function (path) { + if (path.slice(-1) === "/") { + path = path.substring(0, path.length - 1); + } + var lastSlash = path.lastIndexOf("/"); + return (lastSlash > 0) ? path.substring(0, lastSlash) : ""; +}; + +/** + * Returns the path with a slash at the end. + * @private + * @param {String} path the path to check. + * @return {String} the path with a trailing slash. + */ +var forceTrailingSlash = function(path) { + // Check the name ends with a / + if (path.slice(-1) !== "/") { + path += "/"; // IE doesn't like substr(-1) + } + return path; +}; + +/** + * Add a (sub) folder in the current folder. + * @private + * @param {string} name the folder's name + * @param {boolean=} [createFolders] If true, automatically create sub + * folders. Defaults to false. + * @return {Object} the new folder. + */ +var folderAdd = function(name, createFolders) { + createFolders = (typeof createFolders !== "undefined") ? createFolders : defaults.createFolders; + + name = forceTrailingSlash(name); + + // Does this folder already exist? + if (!this.files[name]) { + fileAdd.call(this, name, null, { + dir: true, + createFolders: createFolders + }); + } + return this.files[name]; +}; + +/** +* Cross-window, cross-Node-context regular expression detection +* @param {Object} object Anything +* @return {Boolean} true if the object is a regular expression, +* false otherwise +*/ +function isRegExp(object) { + return Object.prototype.toString.call(object) === "[object RegExp]"; +} + +// return the actual prototype of JSZip +var out = { + /** + * @see loadAsync + */ + load: function() { + throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide."); + }, + + + /** + * Call a callback function for each entry at this folder level. + * @param {Function} cb the callback function: + * function (relativePath, file) {...} + * It takes 2 arguments : the relative path and the file. + */ + forEach: function(cb) { + var filename, relativePath, file; + // ignore warning about unwanted properties because this.files is a null prototype object + /* eslint-disable-next-line guard-for-in */ + for (filename in this.files) { + file = this.files[filename]; + relativePath = filename.slice(this.root.length, filename.length); + if (relativePath && filename.slice(0, this.root.length) === this.root) { // the file is in the current root + cb(relativePath, file); // TODO reverse the parameters ? need to be clean AND consistent with the filter search fn... + } + } + }, + + /** + * Filter nested files/folders with the specified function. + * @param {Function} search the predicate to use : + * function (relativePath, file) {...} + * It takes 2 arguments : the relative path and the file. + * @return {Array} An array of matching elements. + */ + filter: function(search) { + var result = []; + this.forEach(function (relativePath, entry) { + if (search(relativePath, entry)) { // the file matches the function + result.push(entry); + } + + }); + return result; + }, + + /** + * Add a file to the zip file, or search a file. + * @param {string|RegExp} name The name of the file to add (if data is defined), + * the name of the file to find (if no data) or a regex to match files. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data The file data, either raw or base64 encoded + * @param {Object} o File options + * @return {JSZip|Object|Array} this JSZip object (when adding a file), + * a file (when searching by string) or an array of files (when searching by regex). + */ + file: function(name, data, o) { + if (arguments.length === 1) { + if (isRegExp(name)) { + var regexp = name; + return this.filter(function(relativePath, file) { + return !file.dir && regexp.test(relativePath); + }); + } + else { // text + var obj = this.files[this.root + name]; + if (obj && !obj.dir) { + return obj; + } else { + return null; + } + } + } + else { // more than one argument : we have data ! + name = this.root + name; + fileAdd.call(this, name, data, o); + } + return this; + }, + + /** + * Add a directory to the zip file, or search. + * @param {String|RegExp} arg The name of the directory to add, or a regex to search folders. + * @return {JSZip} an object with the new directory as the root, or an array containing matching folders. + */ + folder: function(arg) { + if (!arg) { + return this; + } + + if (isRegExp(arg)) { + return this.filter(function(relativePath, file) { + return file.dir && arg.test(relativePath); + }); + } + + // else, name is a new folder + var name = this.root + arg; + var newFolder = folderAdd.call(this, name); + + // Allow chaining by returning a new object with this folder as the root + var ret = this.clone(); + ret.root = newFolder.name; + return ret; + }, + + /** + * Delete a file, or a directory and all sub-files, from the zip + * @param {string} name the name of the file to delete + * @return {JSZip} this JSZip object + */ + remove: function(name) { + name = this.root + name; + var file = this.files[name]; + if (!file) { + // Look for any folders + if (name.slice(-1) !== "/") { + name += "/"; + } + file = this.files[name]; + } + + if (file && !file.dir) { + // file + delete this.files[name]; + } else { + // maybe a folder, delete recursively + var kids = this.filter(function(relativePath, file) { + return file.name.slice(0, name.length) === name; + }); + for (var i = 0; i < kids.length; i++) { + delete this.files[kids[i].name]; + } + } + + return this; + }, + + /** + * @deprecated This method has been removed in JSZip 3.0, please check the upgrade guide. + */ + generate: function() { + throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide."); + }, + + /** + * Generate the complete zip file as an internal stream. + * @param {Object} options the options to generate the zip file : + * - compression, "STORE" by default. + * - type, "base64" by default. Values are : string, base64, uint8array, arraybuffer, blob. + * @return {StreamHelper} the streamed zip file. + */ + generateInternalStream: function(options) { + var worker, opts = {}; + try { + opts = utils.extend(options || {}, { + streamFiles: false, + compression: "STORE", + compressionOptions : null, + type: "", + platform: "DOS", + comment: null, + mimeType: "application/zip", + encodeFileName: utf8.utf8encode + }); + + opts.type = opts.type.toLowerCase(); + opts.compression = opts.compression.toUpperCase(); + + // "binarystring" is preferred but the internals use "string". + if(opts.type === "binarystring") { + opts.type = "string"; + } + + if (!opts.type) { + throw new Error("No output type specified."); + } + + utils.checkSupport(opts.type); + + // accept nodejs `process.platform` + if( + opts.platform === "darwin" || + opts.platform === "freebsd" || + opts.platform === "linux" || + opts.platform === "sunos" + ) { + opts.platform = "UNIX"; + } + if (opts.platform === "win32") { + opts.platform = "DOS"; + } + + var comment = opts.comment || this.comment || ""; + worker = generate.generateWorker(this, opts, comment); + } catch (e) { + worker = new GenericWorker("error"); + worker.error(e); + } + return new StreamHelper(worker, opts.type || "string", opts.mimeType); + }, + /** + * Generate the complete zip file asynchronously. + * @see generateInternalStream + */ + generateAsync: function(options, onUpdate) { + return this.generateInternalStream(options).accumulate(onUpdate); + }, + /** + * Generate the complete zip file asynchronously. + * @see generateInternalStream + */ + generateNodeStream: function(options, onUpdate) { + options = options || {}; + if (!options.type) { + options.type = "nodebuffer"; + } + return this.generateInternalStream(options).toNodejsStream(onUpdate); + } +}; +module.exports = out; + +},{"./compressedObject":2,"./defaults":5,"./generate":9,"./nodejs/NodejsStreamInputAdapter":12,"./nodejsUtils":14,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31,"./utils":32,"./zipObject":35}],16:[function(require,module,exports){ +"use strict"; +/* + * This file is used by module bundlers (browserify/webpack/etc) when + * including a stream implementation. We use "readable-stream" to get a + * consistent behavior between nodejs versions but bundlers often have a shim + * for "stream". Using this shim greatly improve the compatibility and greatly + * reduce the final size of the bundle (only one stream implementation, not + * two). + */ +module.exports = require("stream"); + +},{"stream":undefined}],17:[function(require,module,exports){ +"use strict"; +var DataReader = require("./DataReader"); +var utils = require("../utils"); + +function ArrayReader(data) { + DataReader.call(this, data); + for(var i = 0; i < this.data.length; i++) { + data[i] = data[i] & 0xFF; + } +} +utils.inherits(ArrayReader, DataReader); +/** + * @see DataReader.byteAt + */ +ArrayReader.prototype.byteAt = function(i) { + return this.data[this.zero + i]; +}; +/** + * @see DataReader.lastIndexOfSignature + */ +ArrayReader.prototype.lastIndexOfSignature = function(sig) { + var sig0 = sig.charCodeAt(0), + sig1 = sig.charCodeAt(1), + sig2 = sig.charCodeAt(2), + sig3 = sig.charCodeAt(3); + for (var i = this.length - 4; i >= 0; --i) { + if (this.data[i] === sig0 && this.data[i + 1] === sig1 && this.data[i + 2] === sig2 && this.data[i + 3] === sig3) { + return i - this.zero; + } + } + + return -1; +}; +/** + * @see DataReader.readAndCheckSignature + */ +ArrayReader.prototype.readAndCheckSignature = function (sig) { + var sig0 = sig.charCodeAt(0), + sig1 = sig.charCodeAt(1), + sig2 = sig.charCodeAt(2), + sig3 = sig.charCodeAt(3), + data = this.readData(4); + return sig0 === data[0] && sig1 === data[1] && sig2 === data[2] && sig3 === data[3]; +}; +/** + * @see DataReader.readData + */ +ArrayReader.prototype.readData = function(size) { + this.checkOffset(size); + if(size === 0) { + return []; + } + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); + this.index += size; + return result; +}; +module.exports = ArrayReader; + +},{"../utils":32,"./DataReader":18}],18:[function(require,module,exports){ +"use strict"; +var utils = require("../utils"); function DataReader(data) { - this.data = null; // type : see implementation - this.length = 0; + this.data = data; // type : see implementation + this.length = data.length; this.index = 0; + this.zero = 0; } DataReader.prototype = { /** @@ -261,12 +1824,12 @@ DataReader.prototype = { this.checkIndex(this.index + offset); }, /** - * Check that the specifed index will not be too far. + * Check that the specified index will not be too far. * @param {string} newIndex the index to check. * @throws {Error} an Error if the index is out of bounds. */ checkIndex: function(newIndex) { - if (this.length < newIndex || newIndex < 0) { + if (this.length < this.zero + newIndex || newIndex < 0) { throw new Error("End of data reached (data length = " + this.length + ", asked index = " + (newIndex) + "). Corrupted zip ?"); } }, @@ -292,7 +1855,7 @@ DataReader.prototype = { * @param {number} i the index to use. * @return {number} a byte. */ - byteAt: function(i) { + byteAt: function() { // see implementations }, /** @@ -323,15 +1886,23 @@ DataReader.prototype = { * @param {number} size the number of bytes to read. * @return {Object} the raw data, implementation specific. */ - readData: function(size) { + readData: function() { // see implementations }, /** - * Find the last occurence of a zip signature (4 bytes). + * Find the last occurrence of a zip signature (4 bytes). * @param {string} sig the signature to find. - * @return {number} the index of the last occurence, -1 if not found. + * @return {number} the index of the last occurrence, -1 if not found. */ - lastIndexOfSignature: function(sig) { + lastIndexOfSignature: function() { + // see implementations + }, + /** + * Read the signature (4 bytes) at the current position and compare it with sig. + * @param {string} sig the expected signature + * @return {boolean} true if the signature matches, false otherwise. + */ + readAndCheckSignature: function() { // see implementations }, /** @@ -340,1221 +1911,65 @@ DataReader.prototype = { */ readDate: function() { var dostime = this.readInt(4); - return new Date( - ((dostime >> 25) & 0x7f) + 1980, // year - ((dostime >> 21) & 0x0f) - 1, // month - (dostime >> 16) & 0x1f, // day - (dostime >> 11) & 0x1f, // hour - (dostime >> 5) & 0x3f, // minute - (dostime & 0x1f) << 1); // second + return new Date(Date.UTC( + ((dostime >> 25) & 0x7f) + 1980, // year + ((dostime >> 21) & 0x0f) - 1, // month + (dostime >> 16) & 0x1f, // day + (dostime >> 11) & 0x1f, // hour + (dostime >> 5) & 0x3f, // minute + (dostime & 0x1f) << 1)); // second } }; module.exports = DataReader; -},{"./utils":21}],6:[function(_dereq_,module,exports){ -'use strict'; -exports.base64 = false; -exports.binary = false; -exports.dir = false; -exports.createFolders = false; -exports.date = null; -exports.compression = null; -exports.compressionOptions = null; -exports.comment = null; -exports.unixPermissions = null; -exports.dosPermissions = null; - -},{}],7:[function(_dereq_,module,exports){ -'use strict'; -var utils = _dereq_('./utils'); - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.string2binary = function(str) { - return utils.string2binary(str); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.string2Uint8Array = function(str) { - return utils.transformTo("uint8array", str); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.uint8Array2String = function(array) { - return utils.transformTo("string", array); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.string2Blob = function(str) { - var buffer = utils.transformTo("arraybuffer", str); - return utils.arrayBuffer2Blob(buffer); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.arrayBuffer2Blob = function(buffer) { - return utils.arrayBuffer2Blob(buffer); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.transformTo = function(outputType, input) { - return utils.transformTo(outputType, input); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.getTypeOf = function(input) { - return utils.getTypeOf(input); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.checkSupport = function(type) { - return utils.checkSupport(type); -}; - -/** - * @deprecated - * This value will be removed in a future version without replacement. - */ -exports.MAX_VALUE_16BITS = utils.MAX_VALUE_16BITS; - -/** - * @deprecated - * This value will be removed in a future version without replacement. - */ -exports.MAX_VALUE_32BITS = utils.MAX_VALUE_32BITS; - - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.pretty = function(str) { - return utils.pretty(str); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.findCompression = function(compressionMethod) { - return utils.findCompression(compressionMethod); -}; - -/** - * @deprecated - * This function will be removed in a future version without replacement. - */ -exports.isRegExp = function (object) { - return utils.isRegExp(object); -}; - - -},{"./utils":21}],8:[function(_dereq_,module,exports){ -'use strict'; -var USE_TYPEDARRAY = (typeof Uint8Array !== 'undefined') && (typeof Uint16Array !== 'undefined') && (typeof Uint32Array !== 'undefined'); - -var pako = _dereq_("pako"); -exports.uncompressInputType = USE_TYPEDARRAY ? "uint8array" : "array"; -exports.compressInputType = USE_TYPEDARRAY ? "uint8array" : "array"; - -exports.magic = "\x08\x00"; -exports.compress = function(input, compressionOptions) { - return pako.deflateRaw(input, { - level : compressionOptions.level || -1 // default compression - }); -}; -exports.uncompress = function(input) { - return pako.inflateRaw(input); -}; - -},{"pako":24}],9:[function(_dereq_,module,exports){ -'use strict'; - -var base64 = _dereq_('./base64'); - -/** -Usage: - zip = new JSZip(); - zip.file("hello.txt", "Hello, World!").file("tempfile", "nothing"); - zip.folder("images").file("smile.gif", base64Data, {base64: true}); - zip.file("Xmas.txt", "Ho ho ho !", {date : new Date("December 25, 2007 00:00:01")}); - zip.remove("tempfile"); - - base64zip = zip.generate(); - -**/ - -/** - * Representation a of zip file in js - * @constructor - * @param {String=|ArrayBuffer=|Uint8Array=} data the data to load, if any (optional). - * @param {Object=} options the options for creating this objects (optional). - */ -function JSZip(data, options) { - // if this constructor is used without `new`, it adds `new` before itself: - if(!(this instanceof JSZip)) return new JSZip(data, options); - - // object containing the files : - // { - // "folder/" : {...}, - // "folder/data.txt" : {...} - // } - this.files = {}; - - this.comment = null; - - // Where we are in the hierarchy - this.root = ""; - if (data) { - this.load(data, options); - } - this.clone = function() { - var newObj = new JSZip(); - for (var i in this) { - if (typeof this[i] !== "function") { - newObj[i] = this[i]; - } - } - return newObj; - }; -} -JSZip.prototype = _dereq_('./object'); -JSZip.prototype.load = _dereq_('./load'); -JSZip.support = _dereq_('./support'); -JSZip.defaults = _dereq_('./defaults'); - -/** - * @deprecated - * This namespace will be removed in a future version without replacement. - */ -JSZip.utils = _dereq_('./deprecatedPublicUtils'); - -JSZip.base64 = { - /** - * @deprecated - * This method will be removed in a future version without replacement. - */ - encode : function(input) { - return base64.encode(input); - }, - /** - * @deprecated - * This method will be removed in a future version without replacement. - */ - decode : function(input) { - return base64.decode(input); - } -}; -JSZip.compressions = _dereq_('./compressions'); -module.exports = JSZip; - -},{"./base64":1,"./compressions":3,"./defaults":6,"./deprecatedPublicUtils":7,"./load":10,"./object":13,"./support":17}],10:[function(_dereq_,module,exports){ -'use strict'; -var base64 = _dereq_('./base64'); -var ZipEntries = _dereq_('./zipEntries'); -module.exports = function(data, options) { - var files, zipEntries, i, input; - options = options || {}; - if (options.base64) { - data = base64.decode(data); - } - - zipEntries = new ZipEntries(data, options); - files = zipEntries.files; - for (i = 0; i < files.length; i++) { - input = files[i]; - this.file(input.fileName, input.decompressed, { - binary: true, - optimizedBinaryString: true, - date: input.date, - dir: input.dir, - comment : input.fileComment.length ? input.fileComment : null, - unixPermissions : input.unixPermissions, - dosPermissions : input.dosPermissions, - createFolders: options.createFolders - }); - } - if (zipEntries.zipComment.length) { - this.comment = zipEntries.zipComment; - } - - return this; -}; - -},{"./base64":1,"./zipEntries":22}],11:[function(_dereq_,module,exports){ -(function (Buffer){ -'use strict'; -module.exports = function(data, encoding){ - return new Buffer(data, encoding); -}; -module.exports.test = function(b){ - return Buffer.isBuffer(b); -}; - -}).call(this,(typeof Buffer !== "undefined" ? Buffer : undefined)) -},{}],12:[function(_dereq_,module,exports){ -'use strict'; -var Uint8ArrayReader = _dereq_('./uint8ArrayReader'); +},{"../utils":32}],19:[function(require,module,exports){ +"use strict"; +var Uint8ArrayReader = require("./Uint8ArrayReader"); +var utils = require("../utils"); function NodeBufferReader(data) { - this.data = data; - this.length = this.data.length; - this.index = 0; + Uint8ArrayReader.call(this, data); } -NodeBufferReader.prototype = new Uint8ArrayReader(); +utils.inherits(NodeBufferReader, Uint8ArrayReader); /** * @see DataReader.readData */ NodeBufferReader.prototype.readData = function(size) { this.checkOffset(size); - var result = this.data.slice(this.index, this.index + size); + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); this.index += size; return result; }; module.exports = NodeBufferReader; -},{"./uint8ArrayReader":18}],13:[function(_dereq_,module,exports){ -'use strict'; -var support = _dereq_('./support'); -var utils = _dereq_('./utils'); -var crc32 = _dereq_('./crc32'); -var signature = _dereq_('./signature'); -var defaults = _dereq_('./defaults'); -var base64 = _dereq_('./base64'); -var compressions = _dereq_('./compressions'); -var CompressedObject = _dereq_('./compressedObject'); -var nodeBuffer = _dereq_('./nodeBuffer'); -var utf8 = _dereq_('./utf8'); -var StringWriter = _dereq_('./stringWriter'); -var Uint8ArrayWriter = _dereq_('./uint8ArrayWriter'); +},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(require,module,exports){ +"use strict"; +var DataReader = require("./DataReader"); +var utils = require("../utils"); -/** - * Returns the raw data of a ZipObject, decompress the content if necessary. - * @param {ZipObject} file the file to use. - * @return {String|ArrayBuffer|Uint8Array|Buffer} the data. - */ -var getRawData = function(file) { - if (file._data instanceof CompressedObject) { - file._data = file._data.getContent(); - file.options.binary = true; - file.options.base64 = false; - - if (utils.getTypeOf(file._data) === "uint8array") { - var copy = file._data; - // when reading an arraybuffer, the CompressedObject mechanism will keep it and subarray() a Uint8Array. - // if we request a file in the same format, we might get the same Uint8Array or its ArrayBuffer (the original zip file). - file._data = new Uint8Array(copy.length); - // with an empty Uint8Array, Opera fails with a "Offset larger than array size" - if (copy.length !== 0) { - file._data.set(copy, 0); - } - } - } - return file._data; -}; - -/** - * Returns the data of a ZipObject in a binary form. If the content is an unicode string, encode it. - * @param {ZipObject} file the file to use. - * @return {String|ArrayBuffer|Uint8Array|Buffer} the data. - */ -var getBinaryData = function(file) { - var result = getRawData(file), - type = utils.getTypeOf(result); - if (type === "string") { - if (!file.options.binary) { - // unicode text ! - // unicode string => binary string is a painful process, check if we can avoid it. - if (support.nodebuffer) { - return nodeBuffer(result, "utf-8"); - } - } - return file.asBinary(); - } - return result; -}; - -/** - * Transform this._data into a string. - * @param {function} filter a function String -> String, applied if not null on the result. - * @return {String} the string representing this._data. - */ -var dataToString = function(asUTF8) { - var result = getRawData(this); - if (result === null || typeof result === "undefined") { - return ""; - } - // if the data is a base64 string, we decode it before checking the encoding ! - if (this.options.base64) { - result = base64.decode(result); - } - if (asUTF8 && this.options.binary) { - // JSZip.prototype.utf8decode supports arrays as input - // skip to array => string step, utf8decode will do it. - result = out.utf8decode(result); - } - else { - // no utf8 transformation, do the array => string step. - result = utils.transformTo("string", result); - } - - if (!asUTF8 && !this.options.binary) { - result = utils.transformTo("string", out.utf8encode(result)); - } - return result; -}; -/** - * A simple object representing a file in the zip file. - * @constructor - * @param {string} name the name of the file - * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data - * @param {Object} options the options of the file - */ -var ZipObject = function(name, data, options) { - this.name = name; - this.dir = options.dir; - this.date = options.date; - this.comment = options.comment; - this.unixPermissions = options.unixPermissions; - this.dosPermissions = options.dosPermissions; - - this._data = data; - this.options = options; - - /* - * This object contains initial values for dir and date. - * With them, we can check if the user changed the deprecated metadata in - * `ZipObject#options` or not. - */ - this._initialMetadata = { - dir : options.dir, - date : options.date - }; -}; - -ZipObject.prototype = { - /** - * Return the content as UTF8 string. - * @return {string} the UTF8 string. - */ - asText: function() { - return dataToString.call(this, true); - }, - /** - * Returns the binary content. - * @return {string} the content as binary. - */ - asBinary: function() { - return dataToString.call(this, false); - }, - /** - * Returns the content as a nodejs Buffer. - * @return {Buffer} the content as a Buffer. - */ - asNodeBuffer: function() { - var result = getBinaryData(this); - return utils.transformTo("nodebuffer", result); - }, - /** - * Returns the content as an Uint8Array. - * @return {Uint8Array} the content as an Uint8Array. - */ - asUint8Array: function() { - var result = getBinaryData(this); - return utils.transformTo("uint8array", result); - }, - /** - * Returns the content as an ArrayBuffer. - * @return {ArrayBuffer} the content as an ArrayBufer. - */ - asArrayBuffer: function() { - return this.asUint8Array().buffer; - } -}; - -/** - * Transform an integer into a string in hexadecimal. - * @private - * @param {number} dec the number to convert. - * @param {number} bytes the number of bytes to generate. - * @returns {string} the result. - */ -var decToHex = function(dec, bytes) { - var hex = "", - i; - for (i = 0; i < bytes; i++) { - hex += String.fromCharCode(dec & 0xff); - dec = dec >>> 8; - } - return hex; -}; - -/** - * Merge the objects passed as parameters into a new one. - * @private - * @param {...Object} var_args All objects to merge. - * @return {Object} a new object with the data of the others. - */ -var extend = function() { - var result = {}, i, attr; - for (i = 0; i < arguments.length; i++) { // arguments is not enumerable in some browsers - for (attr in arguments[i]) { - if (arguments[i].hasOwnProperty(attr) && typeof result[attr] === "undefined") { - result[attr] = arguments[i][attr]; - } - } - } - return result; -}; - -/** - * Transforms the (incomplete) options from the user into the complete - * set of options to create a file. - * @private - * @param {Object} o the options from the user. - * @return {Object} the complete set of options. - */ -var prepareFileAttrs = function(o) { - o = o || {}; - if (o.base64 === true && (o.binary === null || o.binary === undefined)) { - o.binary = true; - } - o = extend(o, defaults); - o.date = o.date || new Date(); - if (o.compression !== null) o.compression = o.compression.toUpperCase(); - - return o; -}; - -/** - * Add a file in the current folder. - * @private - * @param {string} name the name of the file - * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data of the file - * @param {Object} o the options of the file - * @return {Object} the new file. - */ -var fileAdd = function(name, data, o) { - // be sure sub folders exist - var dataType = utils.getTypeOf(data), - parent; - - o = prepareFileAttrs(o); - - if (typeof o.unixPermissions === "string") { - o.unixPermissions = parseInt(o.unixPermissions, 8); - } - - // UNX_IFDIR 0040000 see zipinfo.c - if (o.unixPermissions && (o.unixPermissions & 0x4000)) { - o.dir = true; - } - // Bit 4 Directory - if (o.dosPermissions && (o.dosPermissions & 0x0010)) { - o.dir = true; - } - - if (o.dir) { - name = forceTrailingSlash(name); - } - - if (o.createFolders && (parent = parentFolder(name))) { - folderAdd.call(this, parent, true); - } - - if (o.dir || data === null || typeof data === "undefined") { - o.base64 = false; - o.binary = false; - data = null; - dataType = null; - } - else if (dataType === "string") { - if (o.binary && !o.base64) { - // optimizedBinaryString == true means that the file has already been filtered with a 0xFF mask - if (o.optimizedBinaryString !== true) { - // this is a string, not in a base64 format. - // Be sure that this is a correct "binary string" - data = utils.string2binary(data); - } - } - } - else { // arraybuffer, uint8array, ... - o.base64 = false; - o.binary = true; - - if (!dataType && !(data instanceof CompressedObject)) { - throw new Error("The data of '" + name + "' is in an unsupported format !"); - } - - // special case : it's way easier to work with Uint8Array than with ArrayBuffer - if (dataType === "arraybuffer") { - data = utils.transformTo("uint8array", data); - } - } - - var object = new ZipObject(name, data, o); - this.files[name] = object; - return object; -}; - -/** - * Find the parent folder of the path. - * @private - * @param {string} path the path to use - * @return {string} the parent folder, or "" - */ -var parentFolder = function (path) { - if (path.slice(-1) == '/') { - path = path.substring(0, path.length - 1); - } - var lastSlash = path.lastIndexOf('/'); - return (lastSlash > 0) ? path.substring(0, lastSlash) : ""; -}; - - -/** - * Returns the path with a slash at the end. - * @private - * @param {String} path the path to check. - * @return {String} the path with a trailing slash. - */ -var forceTrailingSlash = function(path) { - // Check the name ends with a / - if (path.slice(-1) != "/") { - path += "/"; // IE doesn't like substr(-1) - } - return path; -}; -/** - * Add a (sub) folder in the current folder. - * @private - * @param {string} name the folder's name - * @param {boolean=} [createFolders] If true, automatically create sub - * folders. Defaults to false. - * @return {Object} the new folder. - */ -var folderAdd = function(name, createFolders) { - createFolders = (typeof createFolders !== 'undefined') ? createFolders : false; - - name = forceTrailingSlash(name); - - // Does this folder already exist? - if (!this.files[name]) { - fileAdd.call(this, name, null, { - dir: true, - createFolders: createFolders - }); - } - return this.files[name]; -}; - -/** - * Generate a JSZip.CompressedObject for a given zipOject. - * @param {ZipObject} file the object to read. - * @param {JSZip.compression} compression the compression to use. - * @param {Object} compressionOptions the options to use when compressing. - * @return {JSZip.CompressedObject} the compressed result. - */ -var generateCompressedObjectFrom = function(file, compression, compressionOptions) { - var result = new CompressedObject(), - content; - - // the data has not been decompressed, we might reuse things ! - if (file._data instanceof CompressedObject) { - result.uncompressedSize = file._data.uncompressedSize; - result.crc32 = file._data.crc32; - - if (result.uncompressedSize === 0 || file.dir) { - compression = compressions['STORE']; - result.compressedContent = ""; - result.crc32 = 0; - } - else if (file._data.compressionMethod === compression.magic) { - result.compressedContent = file._data.getCompressedContent(); - } - else { - content = file._data.getContent(); - // need to decompress / recompress - result.compressedContent = compression.compress(utils.transformTo(compression.compressInputType, content), compressionOptions); - } - } - else { - // have uncompressed data - content = getBinaryData(file); - if (!content || content.length === 0 || file.dir) { - compression = compressions['STORE']; - content = ""; - } - result.uncompressedSize = content.length; - result.crc32 = crc32(content); - result.compressedContent = compression.compress(utils.transformTo(compression.compressInputType, content), compressionOptions); - } - - result.compressedSize = result.compressedContent.length; - result.compressionMethod = compression.magic; - - return result; -}; - - - - -/** - * Generate the UNIX part of the external file attributes. - * @param {Object} unixPermissions the unix permissions or null. - * @param {Boolean} isDir true if the entry is a directory, false otherwise. - * @return {Number} a 32 bit integer. - * - * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute : - * - * TTTTsstrwxrwxrwx0000000000ADVSHR - * ^^^^____________________________ file type, see zipinfo.c (UNX_*) - * ^^^_________________________ setuid, setgid, sticky - * ^^^^^^^^^________________ permissions - * ^^^^^^^^^^______ not used ? - * ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only - */ -var generateUnixExternalFileAttr = function (unixPermissions, isDir) { - - var result = unixPermissions; - if (!unixPermissions) { - // I can't use octal values in strict mode, hence the hexa. - // 040775 => 0x41fd - // 0100664 => 0x81b4 - result = isDir ? 0x41fd : 0x81b4; - } - - return (result & 0xFFFF) << 16; -}; - -/** - * Generate the DOS part of the external file attributes. - * @param {Object} dosPermissions the dos permissions or null. - * @param {Boolean} isDir true if the entry is a directory, false otherwise. - * @return {Number} a 32 bit integer. - * - * Bit 0 Read-Only - * Bit 1 Hidden - * Bit 2 System - * Bit 3 Volume Label - * Bit 4 Directory - * Bit 5 Archive - */ -var generateDosExternalFileAttr = function (dosPermissions, isDir) { - - // the dir flag is already set for compatibility - - return (dosPermissions || 0) & 0x3F; -}; - -/** - * Generate the various parts used in the construction of the final zip file. - * @param {string} name the file name. - * @param {ZipObject} file the file content. - * @param {JSZip.CompressedObject} compressedObject the compressed object. - * @param {number} offset the current offset from the start of the zip file. - * @param {String} platform let's pretend we are this platform (change platform dependents fields) - * @return {object} the zip parts. - */ -var generateZipParts = function(name, file, compressedObject, offset, platform) { - var data = compressedObject.compressedContent, - utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)), - comment = file.comment || "", - utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)), - useUTF8ForFileName = utfEncodedFileName.length !== file.name.length, - useUTF8ForComment = utfEncodedComment.length !== comment.length, - o = file.options, - dosTime, - dosDate, - extraFields = "", - unicodePathExtraField = "", - unicodeCommentExtraField = "", - dir, date; - - - // handle the deprecated options.dir - if (file._initialMetadata.dir !== file.dir) { - dir = file.dir; - } else { - dir = o.dir; - } - - // handle the deprecated options.date - if(file._initialMetadata.date !== file.date) { - date = file.date; - } else { - date = o.date; - } - - var extFileAttr = 0; - var versionMadeBy = 0; - if (dir) { - // dos or unix, we set the dos dir flag - extFileAttr |= 0x00010; - } - if(platform === "UNIX") { - versionMadeBy = 0x031E; // UNIX, version 3.0 - extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir); - } else { // DOS or other, fallback to DOS - versionMadeBy = 0x0014; // DOS, version 2.0 - extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir); - } - - // date - // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html - // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html - // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html - - dosTime = date.getHours(); - dosTime = dosTime << 6; - dosTime = dosTime | date.getMinutes(); - dosTime = dosTime << 5; - dosTime = dosTime | date.getSeconds() / 2; - - dosDate = date.getFullYear() - 1980; - dosDate = dosDate << 4; - dosDate = dosDate | (date.getMonth() + 1); - dosDate = dosDate << 5; - dosDate = dosDate | date.getDate(); - - if (useUTF8ForFileName) { - // set the unicode path extra field. unzip needs at least one extra - // field to correctly handle unicode path, so using the path is as good - // as any other information. This could improve the situation with - // other archive managers too. - // This field is usually used without the utf8 flag, with a non - // unicode path in the header (winrar, winzip). This helps (a bit) - // with the messy Windows' default compressed folders feature but - // breaks on p7zip which doesn't seek the unicode path extra field. - // So for now, UTF-8 everywhere ! - unicodePathExtraField = - // Version - decToHex(1, 1) + - // NameCRC32 - decToHex(crc32(utfEncodedFileName), 4) + - // UnicodeName - utfEncodedFileName; - - extraFields += - // Info-ZIP Unicode Path Extra Field - "\x75\x70" + - // size - decToHex(unicodePathExtraField.length, 2) + - // content - unicodePathExtraField; - } - - if(useUTF8ForComment) { - - unicodeCommentExtraField = - // Version - decToHex(1, 1) + - // CommentCRC32 - decToHex(this.crc32(utfEncodedComment), 4) + - // UnicodeName - utfEncodedComment; - - extraFields += - // Info-ZIP Unicode Path Extra Field - "\x75\x63" + - // size - decToHex(unicodeCommentExtraField.length, 2) + - // content - unicodeCommentExtraField; - } - - var header = ""; - - // version needed to extract - header += "\x0A\x00"; - // general purpose bit flag - // set bit 11 if utf8 - header += (useUTF8ForFileName || useUTF8ForComment) ? "\x00\x08" : "\x00\x00"; - // compression method - header += compressedObject.compressionMethod; - // last mod file time - header += decToHex(dosTime, 2); - // last mod file date - header += decToHex(dosDate, 2); - // crc-32 - header += decToHex(compressedObject.crc32, 4); - // compressed size - header += decToHex(compressedObject.compressedSize, 4); - // uncompressed size - header += decToHex(compressedObject.uncompressedSize, 4); - // file name length - header += decToHex(utfEncodedFileName.length, 2); - // extra field length - header += decToHex(extraFields.length, 2); - - - var fileRecord = signature.LOCAL_FILE_HEADER + header + utfEncodedFileName + extraFields; - - var dirRecord = signature.CENTRAL_FILE_HEADER + - // version made by (00: DOS) - decToHex(versionMadeBy, 2) + - // file header (common to file and central directory) - header + - // file comment length - decToHex(utfEncodedComment.length, 2) + - // disk number start - "\x00\x00" + - // internal file attributes TODO - "\x00\x00" + - // external file attributes - decToHex(extFileAttr, 4) + - // relative offset of local header - decToHex(offset, 4) + - // file name - utfEncodedFileName + - // extra field - extraFields + - // file comment - utfEncodedComment; - - return { - fileRecord: fileRecord, - dirRecord: dirRecord, - compressedObject: compressedObject - }; -}; - - -// return the actual prototype of JSZip -var out = { - /** - * Read an existing zip and merge the data in the current JSZip object. - * The implementation is in jszip-load.js, don't forget to include it. - * @param {String|ArrayBuffer|Uint8Array|Buffer} stream The stream to load - * @param {Object} options Options for loading the stream. - * options.base64 : is the stream in base64 ? default : false - * @return {JSZip} the current JSZip object - */ - load: function(stream, options) { - throw new Error("Load method is not defined. Is the file jszip-load.js included ?"); - }, - - /** - * Filter nested files/folders with the specified function. - * @param {Function} search the predicate to use : - * function (relativePath, file) {...} - * It takes 2 arguments : the relative path and the file. - * @return {Array} An array of matching elements. - */ - filter: function(search) { - var result = [], - filename, relativePath, file, fileClone; - for (filename in this.files) { - if (!this.files.hasOwnProperty(filename)) { - continue; - } - file = this.files[filename]; - // return a new object, don't let the user mess with our internal objects :) - fileClone = new ZipObject(file.name, file._data, extend(file.options)); - relativePath = filename.slice(this.root.length, filename.length); - if (filename.slice(0, this.root.length) === this.root && // the file is in the current root - search(relativePath, fileClone)) { // and the file matches the function - result.push(fileClone); - } - } - return result; - }, - - /** - * Add a file to the zip file, or search a file. - * @param {string|RegExp} name The name of the file to add (if data is defined), - * the name of the file to find (if no data) or a regex to match files. - * @param {String|ArrayBuffer|Uint8Array|Buffer} data The file data, either raw or base64 encoded - * @param {Object} o File options - * @return {JSZip|Object|Array} this JSZip object (when adding a file), - * a file (when searching by string) or an array of files (when searching by regex). - */ - file: function(name, data, o) { - if (arguments.length === 1) { - if (utils.isRegExp(name)) { - var regexp = name; - return this.filter(function(relativePath, file) { - return !file.dir && regexp.test(relativePath); - }); - } - else { // text - return this.filter(function(relativePath, file) { - return !file.dir && relativePath === name; - })[0] || null; - } - } - else { // more than one argument : we have data ! - name = this.root + name; - fileAdd.call(this, name, data, o); - } - return this; - }, - - /** - * Add a directory to the zip file, or search. - * @param {String|RegExp} arg The name of the directory to add, or a regex to search folders. - * @return {JSZip} an object with the new directory as the root, or an array containing matching folders. - */ - folder: function(arg) { - if (!arg) { - return this; - } - - if (utils.isRegExp(arg)) { - return this.filter(function(relativePath, file) { - return file.dir && arg.test(relativePath); - }); - } - - // else, name is a new folder - var name = this.root + arg; - var newFolder = folderAdd.call(this, name); - - // Allow chaining by returning a new object with this folder as the root - var ret = this.clone(); - ret.root = newFolder.name; - return ret; - }, - - /** - * Delete a file, or a directory and all sub-files, from the zip - * @param {string} name the name of the file to delete - * @return {JSZip} this JSZip object - */ - remove: function(name) { - name = this.root + name; - var file = this.files[name]; - if (!file) { - // Look for any folders - if (name.slice(-1) != "/") { - name += "/"; - } - file = this.files[name]; - } - - if (file && !file.dir) { - // file - delete this.files[name]; - } else { - // maybe a folder, delete recursively - var kids = this.filter(function(relativePath, file) { - return file.name.slice(0, name.length) === name; - }); - for (var i = 0; i < kids.length; i++) { - delete this.files[kids[i].name]; - } - } - - return this; - }, - - /** - * Generate the complete zip file - * @param {Object} options the options to generate the zip file : - * - base64, (deprecated, use type instead) true to generate base64. - * - compression, "STORE" by default. - * - type, "base64" by default. Values are : string, base64, uint8array, arraybuffer, blob. - * @return {String|Uint8Array|ArrayBuffer|Buffer|Blob} the zip file - */ - generate: function(options) { - options = extend(options || {}, { - base64: true, - compression: "STORE", - compressionOptions : null, - type: "base64", - platform: "DOS", - comment: null, - mimeType: 'application/zip' - }); - - utils.checkSupport(options.type); - - // accept nodejs `process.platform` - if( - options.platform === 'darwin' || - options.platform === 'freebsd' || - options.platform === 'linux' || - options.platform === 'sunos' - ) { - options.platform = "UNIX"; - } - if (options.platform === 'win32') { - options.platform = "DOS"; - } - - var zipData = [], - localDirLength = 0, - centralDirLength = 0, - writer, i, - utfEncodedComment = utils.transformTo("string", this.utf8encode(options.comment || this.comment || "")); - - // first, generate all the zip parts. - for (var name in this.files) { - if (!this.files.hasOwnProperty(name)) { - continue; - } - var file = this.files[name]; - - var compressionName = file.options.compression || options.compression.toUpperCase(); - var compression = compressions[compressionName]; - if (!compression) { - throw new Error(compressionName + " is not a valid compression method !"); - } - var compressionOptions = file.options.compressionOptions || options.compressionOptions || {}; - - var compressedObject = generateCompressedObjectFrom.call(this, file, compression, compressionOptions); - - var zipPart = generateZipParts.call(this, name, file, compressedObject, localDirLength, options.platform); - localDirLength += zipPart.fileRecord.length + compressedObject.compressedSize; - centralDirLength += zipPart.dirRecord.length; - zipData.push(zipPart); - } - - var dirEnd = ""; - - // end of central dir signature - dirEnd = signature.CENTRAL_DIRECTORY_END + - // number of this disk - "\x00\x00" + - // number of the disk with the start of the central directory - "\x00\x00" + - // total number of entries in the central directory on this disk - decToHex(zipData.length, 2) + - // total number of entries in the central directory - decToHex(zipData.length, 2) + - // size of the central directory 4 bytes - decToHex(centralDirLength, 4) + - // offset of start of central directory with respect to the starting disk number - decToHex(localDirLength, 4) + - // .ZIP file comment length - decToHex(utfEncodedComment.length, 2) + - // .ZIP file comment - utfEncodedComment; - - - // we have all the parts (and the total length) - // time to create a writer ! - var typeName = options.type.toLowerCase(); - if(typeName==="uint8array"||typeName==="arraybuffer"||typeName==="blob"||typeName==="nodebuffer") { - writer = new Uint8ArrayWriter(localDirLength + centralDirLength + dirEnd.length); - }else{ - writer = new StringWriter(localDirLength + centralDirLength + dirEnd.length); - } - - for (i = 0; i < zipData.length; i++) { - writer.append(zipData[i].fileRecord); - writer.append(zipData[i].compressedObject.compressedContent); - } - for (i = 0; i < zipData.length; i++) { - writer.append(zipData[i].dirRecord); - } - - writer.append(dirEnd); - - var zip = writer.finalize(); - - - - switch(options.type.toLowerCase()) { - // case "zip is an Uint8Array" - case "uint8array" : - case "arraybuffer" : - case "nodebuffer" : - return utils.transformTo(options.type.toLowerCase(), zip); - case "blob" : - return utils.arrayBuffer2Blob(utils.transformTo("arraybuffer", zip), options.mimeType); - // case "zip is a string" - case "base64" : - return (options.base64) ? base64.encode(zip) : zip; - default : // case "string" : - return zip; - } - - }, - - /** - * @deprecated - * This method will be removed in a future version without replacement. - */ - crc32: function (input, crc) { - return crc32(input, crc); - }, - - /** - * @deprecated - * This method will be removed in a future version without replacement. - */ - utf8encode: function (string) { - return utils.transformTo("string", utf8.utf8encode(string)); - }, - - /** - * @deprecated - * This method will be removed in a future version without replacement. - */ - utf8decode: function (input) { - return utf8.utf8decode(input); - } -}; -module.exports = out; - -},{"./base64":1,"./compressedObject":2,"./compressions":3,"./crc32":4,"./defaults":6,"./nodeBuffer":11,"./signature":14,"./stringWriter":16,"./support":17,"./uint8ArrayWriter":19,"./utf8":20,"./utils":21}],14:[function(_dereq_,module,exports){ -'use strict'; -exports.LOCAL_FILE_HEADER = "PK\x03\x04"; -exports.CENTRAL_FILE_HEADER = "PK\x01\x02"; -exports.CENTRAL_DIRECTORY_END = "PK\x05\x06"; -exports.ZIP64_CENTRAL_DIRECTORY_LOCATOR = "PK\x06\x07"; -exports.ZIP64_CENTRAL_DIRECTORY_END = "PK\x06\x06"; -exports.DATA_DESCRIPTOR = "PK\x07\x08"; - -},{}],15:[function(_dereq_,module,exports){ -'use strict'; -var DataReader = _dereq_('./dataReader'); -var utils = _dereq_('./utils'); - -function StringReader(data, optimizedBinaryString) { - this.data = data; - if (!optimizedBinaryString) { - this.data = utils.string2binary(this.data); - } - this.length = this.data.length; - this.index = 0; +function StringReader(data) { + DataReader.call(this, data); } -StringReader.prototype = new DataReader(); +utils.inherits(StringReader, DataReader); /** * @see DataReader.byteAt */ StringReader.prototype.byteAt = function(i) { - return this.data.charCodeAt(i); + return this.data.charCodeAt(this.zero + i); }; /** * @see DataReader.lastIndexOfSignature */ StringReader.prototype.lastIndexOfSignature = function(sig) { - return this.data.lastIndexOf(sig); + return this.data.lastIndexOf(sig) - this.zero; +}; +/** + * @see DataReader.readAndCheckSignature + */ +StringReader.prototype.readAndCheckSignature = function (sig) { + var data = this.readData(4); + return sig === data; }; /** * @see DataReader.readData @@ -1562,54 +1977,766 @@ StringReader.prototype.lastIndexOfSignature = function(sig) { StringReader.prototype.readData = function(size) { this.checkOffset(size); // this will work because the constructor applied the "& 0xff" mask. - var result = this.data.slice(this.index, this.index + size); + var result = this.data.slice(this.zero + this.index, this.zero + this.index + size); this.index += size; return result; }; module.exports = StringReader; -},{"./dataReader":5,"./utils":21}],16:[function(_dereq_,module,exports){ -'use strict'; +},{"../utils":32,"./DataReader":18}],21:[function(require,module,exports){ +"use strict"; +var ArrayReader = require("./ArrayReader"); +var utils = require("../utils"); -var utils = _dereq_('./utils'); +function Uint8ArrayReader(data) { + ArrayReader.call(this, data); +} +utils.inherits(Uint8ArrayReader, ArrayReader); +/** + * @see DataReader.readData + */ +Uint8ArrayReader.prototype.readData = function(size) { + this.checkOffset(size); + if(size === 0) { + // in IE10, when using subarray(idx, idx), we get the array [0x00] instead of []. + return new Uint8Array(0); + } + var result = this.data.subarray(this.zero + this.index, this.zero + this.index + size); + this.index += size; + return result; +}; +module.exports = Uint8ArrayReader; + +},{"../utils":32,"./ArrayReader":17}],22:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var support = require("../support"); +var ArrayReader = require("./ArrayReader"); +var StringReader = require("./StringReader"); +var NodeBufferReader = require("./NodeBufferReader"); +var Uint8ArrayReader = require("./Uint8ArrayReader"); /** - * An object to write any content to a string. + * Create a reader adapted to the data. + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data to read. + * @return {DataReader} the data reader. + */ +module.exports = function (data) { + var type = utils.getTypeOf(data); + utils.checkSupport(type); + if (type === "string" && !support.uint8array) { + return new StringReader(data); + } + if (type === "nodebuffer") { + return new NodeBufferReader(data); + } + if (support.uint8array) { + return new Uint8ArrayReader(utils.transformTo("uint8array", data)); + } + return new ArrayReader(utils.transformTo("array", data)); +}; + +},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(require,module,exports){ +"use strict"; +exports.LOCAL_FILE_HEADER = "PK\x03\x04"; +exports.CENTRAL_FILE_HEADER = "PK\x01\x02"; +exports.CENTRAL_DIRECTORY_END = "PK\x05\x06"; +exports.ZIP64_CENTRAL_DIRECTORY_LOCATOR = "PK\x06\x07"; +exports.ZIP64_CENTRAL_DIRECTORY_END = "PK\x06\x06"; +exports.DATA_DESCRIPTOR = "PK\x07\x08"; + +},{}],24:[function(require,module,exports){ +"use strict"; + +var GenericWorker = require("./GenericWorker"); +var utils = require("../utils"); + +/** + * A worker which convert chunks to a specified type. + * @constructor + * @param {String} destType the destination type. + */ +function ConvertWorker(destType) { + GenericWorker.call(this, "ConvertWorker to " + destType); + this.destType = destType; +} +utils.inherits(ConvertWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +ConvertWorker.prototype.processChunk = function (chunk) { + this.push({ + data : utils.transformTo(this.destType, chunk.data), + meta : chunk.meta + }); +}; +module.exports = ConvertWorker; + +},{"../utils":32,"./GenericWorker":28}],25:[function(require,module,exports){ +"use strict"; + +var GenericWorker = require("./GenericWorker"); +var crc32 = require("../crc32"); +var utils = require("../utils"); + +/** + * A worker which calculate the crc32 of the data flowing through. * @constructor */ -var StringWriter = function() { - this.data = []; +function Crc32Probe() { + GenericWorker.call(this, "Crc32Probe"); + this.withStreamInfo("crc32", 0); +} +utils.inherits(Crc32Probe, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +Crc32Probe.prototype.processChunk = function (chunk) { + this.streamInfo.crc32 = crc32(chunk.data, this.streamInfo.crc32 || 0); + this.push(chunk); }; -StringWriter.prototype = { - /** - * Append any content to the current string. - * @param {Object} input the content to add. - */ - append: function(input) { - input = utils.transformTo("string", input); - this.data.push(input); - }, - /** - * Finalize the construction an return the result. - * @return {string} the generated string. - */ - finalize: function() { - return this.data.join(""); +module.exports = Crc32Probe; + +},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("./GenericWorker"); + +/** + * A worker which calculate the total length of the data flowing through. + * @constructor + * @param {String} propName the name used to expose the length + */ +function DataLengthProbe(propName) { + GenericWorker.call(this, "DataLengthProbe for " + propName); + this.propName = propName; + this.withStreamInfo(propName, 0); +} +utils.inherits(DataLengthProbe, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +DataLengthProbe.prototype.processChunk = function (chunk) { + if(chunk) { + var length = this.streamInfo[this.propName] || 0; + this.streamInfo[this.propName] = length + chunk.data.length; + } + GenericWorker.prototype.processChunk.call(this, chunk); +}; +module.exports = DataLengthProbe; + + +},{"../utils":32,"./GenericWorker":28}],27:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var GenericWorker = require("./GenericWorker"); + +// the size of the generated chunks +// TODO expose this as a public variable +var DEFAULT_BLOCK_SIZE = 16 * 1024; + +/** + * A worker that reads a content and emits chunks. + * @constructor + * @param {Promise} dataP the promise of the data to split + */ +function DataWorker(dataP) { + GenericWorker.call(this, "DataWorker"); + var self = this; + this.dataIsReady = false; + this.index = 0; + this.max = 0; + this.data = null; + this.type = ""; + + this._tickScheduled = false; + + dataP.then(function (data) { + self.dataIsReady = true; + self.data = data; + self.max = data && data.length || 0; + self.type = utils.getTypeOf(data); + if(!self.isPaused) { + self._tickAndRepeat(); + } + }, function (e) { + self.error(e); + }); +} + +utils.inherits(DataWorker, GenericWorker); + +/** + * @see GenericWorker.cleanUp + */ +DataWorker.prototype.cleanUp = function () { + GenericWorker.prototype.cleanUp.call(this); + this.data = null; +}; + +/** + * @see GenericWorker.resume + */ +DataWorker.prototype.resume = function () { + if(!GenericWorker.prototype.resume.call(this)) { + return false; + } + + if (!this._tickScheduled && this.dataIsReady) { + this._tickScheduled = true; + utils.delay(this._tickAndRepeat, [], this); + } + return true; +}; + +/** + * Trigger a tick a schedule an other call to this function. + */ +DataWorker.prototype._tickAndRepeat = function() { + this._tickScheduled = false; + if(this.isPaused || this.isFinished) { + return; + } + this._tick(); + if(!this.isFinished) { + utils.delay(this._tickAndRepeat, [], this); + this._tickScheduled = true; } }; -module.exports = StringWriter; +/** + * Read and push a chunk. + */ +DataWorker.prototype._tick = function() { + + if(this.isPaused || this.isFinished) { + return false; + } + + var size = DEFAULT_BLOCK_SIZE; + var data = null, nextIndex = Math.min(this.max, this.index + size); + if (this.index >= this.max) { + // EOF + return this.end(); + } else { + switch(this.type) { + case "string": + data = this.data.substring(this.index, nextIndex); + break; + case "uint8array": + data = this.data.subarray(this.index, nextIndex); + break; + case "array": + case "nodebuffer": + data = this.data.slice(this.index, nextIndex); + break; + } + this.index = nextIndex; + return this.push({ + data : data, + meta : { + percent : this.max ? this.index / this.max * 100 : 0 + } + }); + } +}; + +module.exports = DataWorker; + +},{"../utils":32,"./GenericWorker":28}],28:[function(require,module,exports){ +"use strict"; + +/** + * A worker that does nothing but passing chunks to the next one. This is like + * a nodejs stream but with some differences. On the good side : + * - it works on IE 6-9 without any issue / polyfill + * - it weights less than the full dependencies bundled with browserify + * - it forwards errors (no need to declare an error handler EVERYWHERE) + * + * A chunk is an object with 2 attributes : `meta` and `data`. The former is an + * object containing anything (`percent` for example), see each worker for more + * details. The latter is the real data (String, Uint8Array, etc). + * + * @constructor + * @param {String} name the name of the stream (mainly used for debugging purposes) + */ +function GenericWorker(name) { + // the name of the worker + this.name = name || "default"; + // an object containing metadata about the workers chain + this.streamInfo = {}; + // an error which happened when the worker was paused + this.generatedError = null; + // an object containing metadata to be merged by this worker into the general metadata + this.extraStreamInfo = {}; + // true if the stream is paused (and should not do anything), false otherwise + this.isPaused = true; + // true if the stream is finished (and should not do anything), false otherwise + this.isFinished = false; + // true if the stream is locked to prevent further structure updates (pipe), false otherwise + this.isLocked = false; + // the event listeners + this._listeners = { + "data":[], + "end":[], + "error":[] + }; + // the previous worker, if any + this.previous = null; +} + +GenericWorker.prototype = { + /** + * Push a chunk to the next workers. + * @param {Object} chunk the chunk to push + */ + push : function (chunk) { + this.emit("data", chunk); + }, + /** + * End the stream. + * @return {Boolean} true if this call ended the worker, false otherwise. + */ + end : function () { + if (this.isFinished) { + return false; + } + + this.flush(); + try { + this.emit("end"); + this.cleanUp(); + this.isFinished = true; + } catch (e) { + this.emit("error", e); + } + return true; + }, + /** + * End the stream with an error. + * @param {Error} e the error which caused the premature end. + * @return {Boolean} true if this call ended the worker with an error, false otherwise. + */ + error : function (e) { + if (this.isFinished) { + return false; + } + + if(this.isPaused) { + this.generatedError = e; + } else { + this.isFinished = true; + + this.emit("error", e); + + // in the workers chain exploded in the middle of the chain, + // the error event will go downward but we also need to notify + // workers upward that there has been an error. + if(this.previous) { + this.previous.error(e); + } + + this.cleanUp(); + } + return true; + }, + /** + * Add a callback on an event. + * @param {String} name the name of the event (data, end, error) + * @param {Function} listener the function to call when the event is triggered + * @return {GenericWorker} the current object for chainability + */ + on : function (name, listener) { + this._listeners[name].push(listener); + return this; + }, + /** + * Clean any references when a worker is ending. + */ + cleanUp : function () { + this.streamInfo = this.generatedError = this.extraStreamInfo = null; + this._listeners = []; + }, + /** + * Trigger an event. This will call registered callback with the provided arg. + * @param {String} name the name of the event (data, end, error) + * @param {Object} arg the argument to call the callback with. + */ + emit : function (name, arg) { + if (this._listeners[name]) { + for(var i = 0; i < this._listeners[name].length; i++) { + this._listeners[name][i].call(this, arg); + } + } + }, + /** + * Chain a worker with an other. + * @param {Worker} next the worker receiving events from the current one. + * @return {worker} the next worker for chainability + */ + pipe : function (next) { + return next.registerPrevious(this); + }, + /** + * Same as `pipe` in the other direction. + * Using an API with `pipe(next)` is very easy. + * Implementing the API with the point of view of the next one registering + * a source is easier, see the ZipFileWorker. + * @param {Worker} previous the previous worker, sending events to this one + * @return {Worker} the current worker for chainability + */ + registerPrevious : function (previous) { + if (this.isLocked) { + throw new Error("The stream '" + this + "' has already been used."); + } + + // sharing the streamInfo... + this.streamInfo = previous.streamInfo; + // ... and adding our own bits + this.mergeStreamInfo(); + this.previous = previous; + var self = this; + previous.on("data", function (chunk) { + self.processChunk(chunk); + }); + previous.on("end", function () { + self.end(); + }); + previous.on("error", function (e) { + self.error(e); + }); + return this; + }, + /** + * Pause the stream so it doesn't send events anymore. + * @return {Boolean} true if this call paused the worker, false otherwise. + */ + pause : function () { + if(this.isPaused || this.isFinished) { + return false; + } + this.isPaused = true; + + if(this.previous) { + this.previous.pause(); + } + return true; + }, + /** + * Resume a paused stream. + * @return {Boolean} true if this call resumed the worker, false otherwise. + */ + resume : function () { + if(!this.isPaused || this.isFinished) { + return false; + } + this.isPaused = false; + + // if true, the worker tried to resume but failed + var withError = false; + if(this.generatedError) { + this.error(this.generatedError); + withError = true; + } + if(this.previous) { + this.previous.resume(); + } + + return !withError; + }, + /** + * Flush any remaining bytes as the stream is ending. + */ + flush : function () {}, + /** + * Process a chunk. This is usually the method overridden. + * @param {Object} chunk the chunk to process. + */ + processChunk : function(chunk) { + this.push(chunk); + }, + /** + * Add a key/value to be added in the workers chain streamInfo once activated. + * @param {String} key the key to use + * @param {Object} value the associated value + * @return {Worker} the current worker for chainability + */ + withStreamInfo : function (key, value) { + this.extraStreamInfo[key] = value; + this.mergeStreamInfo(); + return this; + }, + /** + * Merge this worker's streamInfo into the chain's streamInfo. + */ + mergeStreamInfo : function () { + for(var key in this.extraStreamInfo) { + if (!Object.prototype.hasOwnProperty.call(this.extraStreamInfo, key)) { + continue; + } + this.streamInfo[key] = this.extraStreamInfo[key]; + } + }, + + /** + * Lock the stream to prevent further updates on the workers chain. + * After calling this method, all calls to pipe will fail. + */ + lock: function () { + if (this.isLocked) { + throw new Error("The stream '" + this + "' has already been used."); + } + this.isLocked = true; + if (this.previous) { + this.previous.lock(); + } + }, + + /** + * + * Pretty print the workers chain. + */ + toString : function () { + var me = "Worker " + this.name; + if (this.previous) { + return this.previous + " -> " + me; + } else { + return me; + } + } +}; + +module.exports = GenericWorker; + +},{}],29:[function(require,module,exports){ +"use strict"; + +var utils = require("../utils"); +var ConvertWorker = require("./ConvertWorker"); +var GenericWorker = require("./GenericWorker"); +var base64 = require("../base64"); +var support = require("../support"); +var external = require("../external"); + +var NodejsStreamOutputAdapter = null; +if (support.nodestream) { + try { + NodejsStreamOutputAdapter = require("../nodejs/NodejsStreamOutputAdapter"); + } catch(e) { + // ignore + } +} + +/** + * Apply the final transformation of the data. If the user wants a Blob for + * example, it's easier to work with an U8intArray and finally do the + * ArrayBuffer/Blob conversion. + * @param {String} type the name of the final type + * @param {String|Uint8Array|Buffer} content the content to transform + * @param {String} mimeType the mime type of the content, if applicable. + * @return {String|Uint8Array|ArrayBuffer|Buffer|Blob} the content in the right format. + */ +function transformZipOutput(type, content, mimeType) { + switch(type) { + case "blob" : + return utils.newBlob(utils.transformTo("arraybuffer", content), mimeType); + case "base64" : + return base64.encode(content); + default : + return utils.transformTo(type, content); + } +} + +/** + * Concatenate an array of data of the given type. + * @param {String} type the type of the data in the given array. + * @param {Array} dataArray the array containing the data chunks to concatenate + * @return {String|Uint8Array|Buffer} the concatenated data + * @throws Error if the asked type is unsupported + */ +function concat (type, dataArray) { + var i, index = 0, res = null, totalLength = 0; + for(i = 0; i < dataArray.length; i++) { + totalLength += dataArray[i].length; + } + switch(type) { + case "string": + return dataArray.join(""); + case "array": + return Array.prototype.concat.apply([], dataArray); + case "uint8array": + res = new Uint8Array(totalLength); + for(i = 0; i < dataArray.length; i++) { + res.set(dataArray[i], index); + index += dataArray[i].length; + } + return res; + case "nodebuffer": + return Buffer.concat(dataArray); + default: + throw new Error("concat : unsupported type '" + type + "'"); + } +} + +/** + * Listen a StreamHelper, accumulate its content and concatenate it into a + * complete block. + * @param {StreamHelper} helper the helper to use. + * @param {Function} updateCallback a callback called on each update. Called + * with one arg : + * - the metadata linked to the update received. + * @return Promise the promise for the accumulation. + */ +function accumulate(helper, updateCallback) { + return new external.Promise(function (resolve, reject){ + var dataArray = []; + var chunkType = helper._internalType, + resultType = helper._outputType, + mimeType = helper._mimeType; + helper + .on("data", function (data, meta) { + dataArray.push(data); + if(updateCallback) { + updateCallback(meta); + } + }) + .on("error", function(err) { + dataArray = []; + reject(err); + }) + .on("end", function (){ + try { + var result = transformZipOutput(resultType, concat(chunkType, dataArray), mimeType); + resolve(result); + } catch (e) { + reject(e); + } + dataArray = []; + }) + .resume(); + }); +} + +/** + * An helper to easily use workers outside of JSZip. + * @constructor + * @param {Worker} worker the worker to wrap + * @param {String} outputType the type of data expected by the use + * @param {String} mimeType the mime type of the content, if applicable. + */ +function StreamHelper(worker, outputType, mimeType) { + var internalType = outputType; + switch(outputType) { + case "blob": + case "arraybuffer": + internalType = "uint8array"; + break; + case "base64": + internalType = "string"; + break; + } + + try { + // the type used internally + this._internalType = internalType; + // the type used to output results + this._outputType = outputType; + // the mime type + this._mimeType = mimeType; + utils.checkSupport(internalType); + this._worker = worker.pipe(new ConvertWorker(internalType)); + // the last workers can be rewired without issues but we need to + // prevent any updates on previous workers. + worker.lock(); + } catch(e) { + this._worker = new GenericWorker("error"); + this._worker.error(e); + } +} + +StreamHelper.prototype = { + /** + * Listen a StreamHelper, accumulate its content and concatenate it into a + * complete block. + * @param {Function} updateCb the update callback. + * @return Promise the promise for the accumulation. + */ + accumulate : function (updateCb) { + return accumulate(this, updateCb); + }, + /** + * Add a listener on an event triggered on a stream. + * @param {String} evt the name of the event + * @param {Function} fn the listener + * @return {StreamHelper} the current helper. + */ + on : function (evt, fn) { + var self = this; + + if(evt === "data") { + this._worker.on(evt, function (chunk) { + fn.call(self, chunk.data, chunk.meta); + }); + } else { + this._worker.on(evt, function () { + utils.delay(fn, arguments, self); + }); + } + return this; + }, + /** + * Resume the flow of chunks. + * @return {StreamHelper} the current helper. + */ + resume : function () { + utils.delay(this._worker.resume, [], this._worker); + return this; + }, + /** + * Pause the flow of chunks. + * @return {StreamHelper} the current helper. + */ + pause : function () { + this._worker.pause(); + return this; + }, + /** + * Return a nodejs stream for this helper. + * @param {Function} updateCb the update callback. + * @return {NodejsStreamOutputAdapter} the nodejs stream. + */ + toNodejsStream : function (updateCb) { + utils.checkSupport("nodestream"); + if (this._outputType !== "nodebuffer") { + // an object stream containing blob/arraybuffer/uint8array/string + // is strange and I don't know if it would be useful. + // I you find this comment and have a good usecase, please open a + // bug report ! + throw new Error(this._outputType + " is not supported by this method"); + } + + return new NodejsStreamOutputAdapter(this, { + objectMode : this._outputType !== "nodebuffer" + }, updateCb); + } +}; + + +module.exports = StreamHelper; + +},{"../base64":1,"../external":6,"../nodejs/NodejsStreamOutputAdapter":13,"../support":30,"../utils":32,"./ConvertWorker":24,"./GenericWorker":28}],30:[function(require,module,exports){ +"use strict"; -},{"./utils":21}],17:[function(_dereq_,module,exports){ -(function (Buffer){ -'use strict'; exports.base64 = true; exports.array = true; exports.string = true; exports.arraybuffer = typeof ArrayBuffer !== "undefined" && typeof Uint8Array !== "undefined"; -// contains true if JSZip can read/generate nodejs Buffer, false otherwise. -// Browserify will provide a Buffer implementation for browsers, which is -// an augmented Uint8Array (i.e., can be used as either Buffer or U8). exports.nodebuffer = typeof Buffer !== "undefined"; // contains true if JSZip can read/generate Uint8Array, false otherwise. exports.uint8array = typeof Uint8Array !== "undefined"; @@ -1626,10 +2753,10 @@ else { } catch (e) { try { - var Builder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; + var Builder = self.BlobBuilder || self.WebKitBlobBuilder || self.MozBlobBuilder || self.MSBlobBuilder; var builder = new Builder(); builder.append(buffer); - exports.blob = builder.getBlob('application/zip').size === 0; + exports.blob = builder.getBlob("application/zip").size === 0; } catch (e) { exports.blob = false; @@ -1637,100 +2764,19 @@ else { } } -}).call(this,(typeof Buffer !== "undefined" ? Buffer : undefined)) -},{}],18:[function(_dereq_,module,exports){ -'use strict'; -var DataReader = _dereq_('./dataReader'); - -function Uint8ArrayReader(data) { - if (data) { - this.data = data; - this.length = this.data.length; - this.index = 0; - } +try { + exports.nodestream = !!require("readable-stream").Readable; +} catch(e) { + exports.nodestream = false; } -Uint8ArrayReader.prototype = new DataReader(); -/** - * @see DataReader.byteAt - */ -Uint8ArrayReader.prototype.byteAt = function(i) { - return this.data[i]; -}; -/** - * @see DataReader.lastIndexOfSignature - */ -Uint8ArrayReader.prototype.lastIndexOfSignature = function(sig) { - var sig0 = sig.charCodeAt(0), - sig1 = sig.charCodeAt(1), - sig2 = sig.charCodeAt(2), - sig3 = sig.charCodeAt(3); - for (var i = this.length - 4; i >= 0; --i) { - if (this.data[i] === sig0 && this.data[i + 1] === sig1 && this.data[i + 2] === sig2 && this.data[i + 3] === sig3) { - return i; - } - } - return -1; -}; -/** - * @see DataReader.readData - */ -Uint8ArrayReader.prototype.readData = function(size) { - this.checkOffset(size); - if(size === 0) { - // in IE10, when using subarray(idx, idx), we get the array [0x00] instead of []. - return new Uint8Array(0); - } - var result = this.data.subarray(this.index, this.index + size); - this.index += size; - return result; -}; -module.exports = Uint8ArrayReader; +},{"readable-stream":16}],31:[function(require,module,exports){ +"use strict"; -},{"./dataReader":5}],19:[function(_dereq_,module,exports){ -'use strict'; - -var utils = _dereq_('./utils'); - -/** - * An object to write any content to an Uint8Array. - * @constructor - * @param {number} length The length of the array. - */ -var Uint8ArrayWriter = function(length) { - this.data = new Uint8Array(length); - this.index = 0; -}; -Uint8ArrayWriter.prototype = { - /** - * Append any content to the current array. - * @param {Object} input the content to add. - */ - append: function(input) { - if (input.length !== 0) { - // with an empty Uint8Array, Opera fails with a "Offset larger than array size" - input = utils.transformTo("uint8array", input); - this.data.set(input, this.index); - this.index += input.length; - } - }, - /** - * Finalize the construction an return the result. - * @return {Uint8Array} the generated array. - */ - finalize: function() { - return this.data; - } -}; - -module.exports = Uint8ArrayWriter; - -},{"./utils":21}],20:[function(_dereq_,module,exports){ -'use strict'; - -var utils = _dereq_('./utils'); -var support = _dereq_('./support'); -var nodeBuffer = _dereq_('./nodeBuffer'); +var utils = require("./utils"); +var support = require("./support"); +var nodejsUtils = require("./nodejsUtils"); +var GenericWorker = require("./stream/GenericWorker"); /** * The following functions come from pako, from pako/lib/utils/strings @@ -1742,7 +2788,7 @@ var nodeBuffer = _dereq_('./nodeBuffer'); // because max possible codepoint is 0x10ffff var _utf8len = new Array(256); for (var i=0; i<256; i++) { - _utf8len[i] = (i >= 252 ? 6 : i >= 248 ? 5 : i >= 240 ? 4 : i >= 224 ? 3 : i >= 192 ? 2 : 1); + _utf8len[i] = (i >= 252 ? 6 : i >= 248 ? 5 : i >= 240 ? 4 : i >= 224 ? 3 : i >= 192 ? 2 : 1); } _utf8len[254]=_utf8len[254]=1; // Invalid sequence start @@ -1833,7 +2879,7 @@ var utf8border = function(buf, max) { // convert array to string var buf2string = function (buf) { - var str, i, out, c, c_len; + var i, out, c, c_len; var len = buf.length; // Reserve max possible length (2 words per char) @@ -1895,7 +2941,7 @@ var buf2string = function (buf) { */ exports.utf8encode = function utf8encode(str) { if (support.nodebuffer) { - return nodeBuffer(str, "utf-8"); + return nodejsUtils.newBufferFrom(str, "utf-8"); } return string2buf(str); @@ -1915,60 +2961,149 @@ exports.utf8decode = function utf8decode(buf) { buf = utils.transformTo(support.uint8array ? "uint8array" : "array", buf); - // return buf2string(buf); - // Chrome prefers to work with "small" chunks of data - // for the method buf2string. - // Firefox and Chrome has their own shortcut, IE doesn't seem to really care. - var result = [], k = 0, len = buf.length, chunk = 65536; - while (k < len) { - var nextBoundary = utf8border(buf, Math.min(k + chunk, len)); - if (support.uint8array) { - result.push(buf2string(buf.subarray(k, nextBoundary))); - } else { - result.push(buf2string(buf.slice(k, nextBoundary))); - } - k = nextBoundary; - } - return result.join(""); - + return buf2string(buf); }; -// vim: set shiftwidth=4 softtabstop=4: -},{"./nodeBuffer":11,"./support":17,"./utils":21}],21:[function(_dereq_,module,exports){ -'use strict'; -var support = _dereq_('./support'); -var compressions = _dereq_('./compressions'); -var nodeBuffer = _dereq_('./nodeBuffer'); /** - * Convert a string to a "binary string" : a string containing only char codes between 0 and 255. - * @param {string} str the string to transform. - * @return {String} the binary string. + * A worker to decode utf8 encoded binary chunks into string chunks. + * @constructor */ -exports.string2binary = function(str) { - var result = ""; - for (var i = 0; i < str.length; i++) { - result += String.fromCharCode(str.charCodeAt(i) & 0xff); +function Utf8DecodeWorker() { + GenericWorker.call(this, "utf-8 decode"); + // the last bytes if a chunk didn't end with a complete codepoint. + this.leftOver = null; +} +utils.inherits(Utf8DecodeWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +Utf8DecodeWorker.prototype.processChunk = function (chunk) { + + var data = utils.transformTo(support.uint8array ? "uint8array" : "array", chunk.data); + + // 1st step, re-use what's left of the previous chunk + if (this.leftOver && this.leftOver.length) { + if(support.uint8array) { + var previousData = data; + data = new Uint8Array(previousData.length + this.leftOver.length); + data.set(this.leftOver, 0); + data.set(previousData, this.leftOver.length); + } else { + data = this.leftOver.concat(data); + } + this.leftOver = null; } - return result; + + var nextBoundary = utf8border(data); + var usableData = data; + if (nextBoundary !== data.length) { + if (support.uint8array) { + usableData = data.subarray(0, nextBoundary); + this.leftOver = data.subarray(nextBoundary, data.length); + } else { + usableData = data.slice(0, nextBoundary); + this.leftOver = data.slice(nextBoundary, data.length); + } + } + + this.push({ + data : exports.utf8decode(usableData), + meta : chunk.meta + }); }; -exports.arrayBuffer2Blob = function(buffer, mimeType) { + +/** + * @see GenericWorker.flush + */ +Utf8DecodeWorker.prototype.flush = function () { + if(this.leftOver && this.leftOver.length) { + this.push({ + data : exports.utf8decode(this.leftOver), + meta : {} + }); + this.leftOver = null; + } +}; +exports.Utf8DecodeWorker = Utf8DecodeWorker; + +/** + * A worker to endcode string chunks into utf8 encoded binary chunks. + * @constructor + */ +function Utf8EncodeWorker() { + GenericWorker.call(this, "utf-8 encode"); +} +utils.inherits(Utf8EncodeWorker, GenericWorker); + +/** + * @see GenericWorker.processChunk + */ +Utf8EncodeWorker.prototype.processChunk = function (chunk) { + this.push({ + data : exports.utf8encode(chunk.data), + meta : chunk.meta + }); +}; +exports.Utf8EncodeWorker = Utf8EncodeWorker; + +},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(require,module,exports){ +"use strict"; + +var support = require("./support"); +var base64 = require("./base64"); +var nodejsUtils = require("./nodejsUtils"); +var external = require("./external"); +require("setimmediate"); + + +/** + * Convert a string that pass as a "binary string": it should represent a byte + * array but may have > 255 char codes. Be sure to take only the first byte + * and returns the byte array. + * @param {String} str the string to transform. + * @return {Array|Uint8Array} the string in a binary format. + */ +function string2binary(str) { + var result = null; + if (support.uint8array) { + result = new Uint8Array(str.length); + } else { + result = new Array(str.length); + } + return stringToArrayLike(str, result); +} + +/** + * Create a new blob with the given content and the given type. + * @param {String|ArrayBuffer} part the content to put in the blob. DO NOT use + * an Uint8Array because the stock browser of android 4 won't accept it (it + * will be silently converted to a string, "[object Uint8Array]"). + * + * Use only ONE part to build the blob to avoid a memory leak in IE11 / Edge: + * when a large amount of Array is used to create the Blob, the amount of + * memory consumed is nearly 100 times the original data amount. + * + * @param {String} type the mime type of the blob. + * @return {Blob} the created blob. + */ +exports.newBlob = function(part, type) { exports.checkSupport("blob"); - mimeType = mimeType || 'application/zip'; try { // Blob constructor - return new Blob([buffer], { - type: mimeType + return new Blob([part], { + type: type }); } catch (e) { try { // deprecated, browser only, old way - var Builder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; + var Builder = self.BlobBuilder || self.WebKitBlobBuilder || self.MozBlobBuilder || self.MSBlobBuilder; var builder = new Builder(); - builder.append(buffer); - return builder.getBlob(mimeType); + builder.append(part); + return builder.getBlob(type); } catch (e) { @@ -2001,6 +3136,76 @@ function stringToArrayLike(str, array) { return array; } +/** + * An helper for the function arrayLikeToString. + * This contains static information and functions that + * can be optimized by the browser JIT compiler. + */ +var arrayToStringHelper = { + /** + * Transform an array of int into a string, chunk by chunk. + * See the performances notes on arrayLikeToString. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform. + * @param {String} type the type of the array. + * @param {Integer} chunk the chunk size. + * @return {String} the resulting string. + * @throws Error if the chunk is too big for the stack. + */ + stringifyByChunk: function(array, type, chunk) { + var result = [], k = 0, len = array.length; + // shortcut + if (len <= chunk) { + return String.fromCharCode.apply(null, array); + } + while (k < len) { + if (type === "array" || type === "nodebuffer") { + result.push(String.fromCharCode.apply(null, array.slice(k, Math.min(k + chunk, len)))); + } + else { + result.push(String.fromCharCode.apply(null, array.subarray(k, Math.min(k + chunk, len)))); + } + k += chunk; + } + return result.join(""); + }, + /** + * Call String.fromCharCode on every item in the array. + * This is the naive implementation, which generate A LOT of intermediate string. + * This should be used when everything else fail. + * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform. + * @return {String} the result. + */ + stringifyByChar: function(array){ + var resultStr = ""; + for(var i = 0; i < array.length; i++) { + resultStr += String.fromCharCode(array[i]); + } + return resultStr; + }, + applyCanBeUsed : { + /** + * true if the browser accepts to use String.fromCharCode on Uint8Array + */ + uint8array : (function () { + try { + return support.uint8array && String.fromCharCode.apply(null, new Uint8Array(1)).length === 1; + } catch (e) { + return false; + } + })(), + /** + * true if the browser accepts to use String.fromCharCode on nodejs Buffer. + */ + nodebuffer : (function () { + try { + return support.nodebuffer && String.fromCharCode.apply(null, nodejsUtils.allocBuffer(1)).length === 1; + } catch (e) { + return false; + } + })() + } +}; + /** * Transform an array-like object to a string. * @param {Array|ArrayBuffer|Uint8Array|Buffer} array the array to transform. @@ -2016,49 +3221,29 @@ function arrayLikeToString(array) { // result += String.fromCharCode(array[i]); generate too many strings ! // // This code is inspired by http://jsperf.com/arraybuffer-to-string-apply-performance/2 - var chunk = 65536; - var result = [], - len = array.length, + // TODO : we now have workers that split the work. Do we still need that ? + var chunk = 65536, type = exports.getTypeOf(array), - k = 0, canUseApply = true; - try { - switch(type) { - case "uint8array": - String.fromCharCode.apply(null, new Uint8Array(0)); - break; - case "nodebuffer": - String.fromCharCode.apply(null, nodeBuffer(0)); - break; - } - } catch(e) { - canUseApply = false; - } + if (type === "uint8array") { + canUseApply = arrayToStringHelper.applyCanBeUsed.uint8array; + } else if (type === "nodebuffer") { + canUseApply = arrayToStringHelper.applyCanBeUsed.nodebuffer; + } - // no apply : slow and painful algorithm - // default browser on android 4.* - if (!canUseApply) { - var resultStr = ""; - for(var i = 0; i < array.length;i++) { - resultStr += String.fromCharCode(array[i]); - } - return resultStr; - } - while (k < len && chunk > 1) { - try { - if (type === "array" || type === "nodebuffer") { - result.push(String.fromCharCode.apply(null, array.slice(k, Math.min(k + chunk, len)))); + if (canUseApply) { + while (chunk > 1) { + try { + return arrayToStringHelper.stringifyByChunk(array, type, chunk); + } catch (e) { + chunk = Math.floor(chunk / 2); } - else { - result.push(String.fromCharCode.apply(null, array.subarray(k, Math.min(k + chunk, len)))); - } - k += chunk; - } - catch (e) { - chunk = Math.floor(chunk / 2); } } - return result.join(""); + + // no apply or chunk error : slow and painful algorithm + // default browser on android 4.* + return arrayToStringHelper.stringifyByChar(array); } exports.applyFromCharCode = arrayLikeToString; @@ -2093,7 +3278,7 @@ transform["string"] = { return stringToArrayLike(input, new Uint8Array(input.length)); }, "nodebuffer": function(input) { - return stringToArrayLike(input, nodeBuffer(input.length)); + return stringToArrayLike(input, nodejsUtils.allocBuffer(input.length)); } }; @@ -2108,7 +3293,7 @@ transform["array"] = { return new Uint8Array(input); }, "nodebuffer": function(input) { - return nodeBuffer(input); + return nodejsUtils.newBufferFrom(input); } }; @@ -2125,7 +3310,7 @@ transform["arraybuffer"] = { return new Uint8Array(input); }, "nodebuffer": function(input) { - return nodeBuffer(new Uint8Array(input)); + return nodejsUtils.newBufferFrom(new Uint8Array(input)); } }; @@ -2140,7 +3325,7 @@ transform["uint8array"] = { }, "uint8array": identity, "nodebuffer": function(input) { - return nodeBuffer(input); + return nodejsUtils.newBufferFrom(input); } }; @@ -2182,6 +3367,31 @@ exports.transformTo = function(outputType, input) { return result; }; +/** + * Resolve all relative path components, "." and "..", in a path. If these relative components + * traverse above the root then the resulting path will only contain the final path component. + * + * All empty components, e.g. "//", are removed. + * @param {string} path A path with / or \ separators + * @returns {string} The path with all relative path components resolved. + */ +exports.resolve = function(path) { + var parts = path.split("/"); + var result = []; + for (var index = 0; index < parts.length; index++) { + var part = parts[index]; + // Allow the first and last component to be empty for trailing slashes. + if (part === "." || (part === "" && index !== 0 && index !== parts.length - 1)) { + continue; + } else if (part === "..") { + result.pop(); + } else { + result.push(part); + } + } + return result.join("/"); +}; + /** * Return the type of the input. * The type will be in a format valid for JSZip.utils.transformTo : string, array, uint8array, arraybuffer. @@ -2195,7 +3405,7 @@ exports.getTypeOf = function(input) { if (Object.prototype.toString.call(input) === "[object Array]") { return "array"; } - if (support.nodebuffer && nodeBuffer.test(input)) { + if (support.nodebuffer && nodejsUtils.isBuffer(input)) { return "nodebuffer"; } if (support.uint8array && input instanceof Uint8Array) { @@ -2214,9 +3424,10 @@ exports.getTypeOf = function(input) { exports.checkSupport = function(type) { var supported = support[type.toLowerCase()]; if (!supported) { - throw new Error(type + " is not supported by this browser"); + throw new Error(type + " is not supported by this platform"); } }; + exports.MAX_VALUE_16BITS = 65535; exports.MAX_VALUE_32BITS = -1; // well, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" is parsed as -1 @@ -2226,78 +3437,163 @@ exports.MAX_VALUE_32BITS = -1; // well, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" is pa * @return {string} a pretty string. */ exports.pretty = function(str) { - var res = '', + var res = "", code, i; for (i = 0; i < (str || "").length; i++) { code = str.charCodeAt(i); - res += '\\x' + (code < 16 ? "0" : "") + code.toString(16).toUpperCase(); + res += "\\x" + (code < 16 ? "0" : "") + code.toString(16).toUpperCase(); } return res; }; /** - * Find a compression registered in JSZip. - * @param {string} compressionMethod the method magic to find. - * @return {Object|null} the JSZip compression object, null if none found. + * Defer the call of a function. + * @param {Function} callback the function to call asynchronously. + * @param {Array} args the arguments to give to the callback. */ -exports.findCompression = function(compressionMethod) { - for (var method in compressions) { - if (!compressions.hasOwnProperty(method)) { - continue; - } - if (compressions[method].magic === compressionMethod) { - return compressions[method]; +exports.delay = function(callback, args, self) { + setImmediate(function () { + callback.apply(self || null, args || []); + }); +}; + +/** + * Extends a prototype with an other, without calling a constructor with + * side effects. Inspired by nodejs' `utils.inherits` + * @param {Function} ctor the constructor to augment + * @param {Function} superCtor the parent constructor to use + */ +exports.inherits = function (ctor, superCtor) { + var Obj = function() {}; + Obj.prototype = superCtor.prototype; + ctor.prototype = new Obj(); +}; + +/** + * Merge the objects passed as parameters into a new one. + * @private + * @param {...Object} var_args All objects to merge. + * @return {Object} a new object with the data of the others. + */ +exports.extend = function() { + var result = {}, i, attr; + for (i = 0; i < arguments.length; i++) { // arguments is not enumerable in some browsers + for (attr in arguments[i]) { + if (Object.prototype.hasOwnProperty.call(arguments[i], attr) && typeof result[attr] === "undefined") { + result[attr] = arguments[i][attr]; + } } } - return null; + return result; }; + /** -* Cross-window, cross-Node-context regular expression detection -* @param {Object} object Anything -* @return {Boolean} true if the object is a regular expression, -* false otherwise -*/ -exports.isRegExp = function (object) { - return Object.prototype.toString.call(object) === "[object RegExp]"; + * Transform arbitrary content into a Promise. + * @param {String} name a name for the content being processed. + * @param {Object} inputData the content to process. + * @param {Boolean} isBinary true if the content is not an unicode string + * @param {Boolean} isOptimizedBinaryString true if the string content only has one byte per character. + * @param {Boolean} isBase64 true if the string content is encoded with base64. + * @return {Promise} a promise in a format usable by JSZip. + */ +exports.prepareContent = function(name, inputData, isBinary, isOptimizedBinaryString, isBase64) { + + // if inputData is already a promise, this flatten it. + var promise = external.Promise.resolve(inputData).then(function(data) { + + + var isBlob = support.blob && (data instanceof Blob || ["[object File]", "[object Blob]"].indexOf(Object.prototype.toString.call(data)) !== -1); + + if (isBlob && typeof FileReader !== "undefined") { + return new external.Promise(function (resolve, reject) { + var reader = new FileReader(); + + reader.onload = function(e) { + resolve(e.target.result); + }; + reader.onerror = function(e) { + reject(e.target.error); + }; + reader.readAsArrayBuffer(data); + }); + } else { + return data; + } + }); + + return promise.then(function(data) { + var dataType = exports.getTypeOf(data); + + if (!dataType) { + return external.Promise.reject( + new Error("Can't read the data of '" + name + "'. Is it " + + "in a supported JavaScript type (String, Blob, ArrayBuffer, etc) ?") + ); + } + // special case : it's way easier to work with Uint8Array than with ArrayBuffer + if (dataType === "arraybuffer") { + data = exports.transformTo("uint8array", data); + } else if (dataType === "string") { + if (isBase64) { + data = base64.decode(data); + } + else if (isBinary) { + // optimizedBinaryString === true means that the file has already been filtered with a 0xFF mask + if (isOptimizedBinaryString !== true) { + // this is a string, not in a base64 format. + // Be sure that this is a correct "binary string" + data = string2binary(data); + } + } + } + return data; + }); }; - -},{"./compressions":3,"./nodeBuffer":11,"./support":17}],22:[function(_dereq_,module,exports){ -'use strict'; -var StringReader = _dereq_('./stringReader'); -var NodeBufferReader = _dereq_('./nodeBufferReader'); -var Uint8ArrayReader = _dereq_('./uint8ArrayReader'); -var utils = _dereq_('./utils'); -var sig = _dereq_('./signature'); -var ZipEntry = _dereq_('./zipEntry'); -var support = _dereq_('./support'); -var jszipProto = _dereq_('./object'); +},{"./base64":1,"./external":6,"./nodejsUtils":14,"./support":30,"setimmediate":54}],33:[function(require,module,exports){ +"use strict"; +var readerFor = require("./reader/readerFor"); +var utils = require("./utils"); +var sig = require("./signature"); +var ZipEntry = require("./zipEntry"); +var support = require("./support"); // class ZipEntries {{{ /** * All the entries in the zip file. * @constructor - * @param {String|ArrayBuffer|Uint8Array} data the binary stream to load. * @param {Object} loadOptions Options for loading the stream. */ -function ZipEntries(data, loadOptions) { +function ZipEntries(loadOptions) { this.files = []; this.loadOptions = loadOptions; - if (data) { - this.load(data); - } } ZipEntries.prototype = { /** - * Check that the reader is on the speficied signature. + * Check that the reader is on the specified signature. * @param {string} expectedSignature the expected signature. * @throws {Error} if it is an other signature. */ checkSignature: function(expectedSignature) { - var signature = this.reader.readString(4); - if (signature !== expectedSignature) { - throw new Error("Corrupted zip or bug : unexpected signature " + "(" + utils.pretty(signature) + ", expected " + utils.pretty(expectedSignature) + ")"); + if (!this.reader.readAndCheckSignature(expectedSignature)) { + this.reader.index -= 4; + var signature = this.reader.readString(4); + throw new Error("Corrupted zip or bug: unexpected signature " + "(" + utils.pretty(signature) + ", expected " + utils.pretty(expectedSignature) + ")"); } }, + /** + * Check if the given signature is at the given index. + * @param {number} askedIndex the index to check. + * @param {string} expectedSignature the signature to expect. + * @return {boolean} true if the signature is here, false otherwise. + */ + isSignature: function(askedIndex, expectedSignature) { + var currentIndex = this.reader.index; + this.reader.setIndex(askedIndex); + var signature = this.reader.readString(4); + var result = signature === expectedSignature; + this.reader.setIndex(currentIndex); + return result; + }, /** * Read the end of the central directory. */ @@ -2313,10 +3609,12 @@ ZipEntries.prototype = { // warning : the encoding depends of the system locale // On a linux machine with LANG=en_US.utf8, this field is utf8 encoded. // On a windows machine, this field is encoded with the localized windows code page. - this.zipComment = this.reader.readString(this.zipCommentLength); + var zipComment = this.reader.readData(this.zipCommentLength); + var decodeParamType = support.uint8array ? "uint8array" : "array"; // To get consistent behavior with the generation part, we will assume that - // this is utf8 encoded. - this.zipComment = jszipProto.utf8decode(this.zipComment); + // this is utf8 encoded unless specified otherwise. + var decodeContent = utils.transformTo(decodeParamType, zipComment); + this.zipComment = this.loadOptions.decodeFileName(decodeContent); }, /** * Read the end of the Zip 64 central directory. @@ -2326,8 +3624,9 @@ ZipEntries.prototype = { */ readBlockZip64EndOfCentral: function() { this.zip64EndOfCentralSize = this.reader.readInt(8); - this.versionMadeBy = this.reader.readString(2); - this.versionNeeded = this.reader.readInt(2); + this.reader.skip(4); + // this.versionMadeBy = this.reader.readString(2); + // this.versionNeeded = this.reader.readInt(2); this.diskNumber = this.reader.readInt(4); this.diskWithCentralDirStart = this.reader.readInt(4); this.centralDirRecordsOnThisDisk = this.reader.readInt(8); @@ -2344,7 +3643,7 @@ ZipEntries.prototype = { while (index < extraDataSize) { extraFieldId = this.reader.readInt(2); extraFieldLength = this.reader.readInt(4); - extraFieldValue = this.reader.readString(extraFieldLength); + extraFieldValue = this.reader.readData(extraFieldLength); this.zip64ExtensibleData[extraFieldId] = { id: extraFieldId, length: extraFieldLength, @@ -2384,40 +3683,49 @@ ZipEntries.prototype = { var file; this.reader.setIndex(this.centralDirOffset); - while (this.reader.readString(4) === sig.CENTRAL_FILE_HEADER) { + while (this.reader.readAndCheckSignature(sig.CENTRAL_FILE_HEADER)) { file = new ZipEntry({ zip64: this.zip64 }, this.loadOptions); file.readCentralPart(this.reader); this.files.push(file); } + + if (this.centralDirRecords !== this.files.length) { + if (this.centralDirRecords !== 0 && this.files.length === 0) { + // We expected some records but couldn't find ANY. + // This is really suspicious, as if something went wrong. + throw new Error("Corrupted zip or bug: expected " + this.centralDirRecords + " records in central dir, got " + this.files.length); + } else { + // We found some records but not all. + // Something is wrong but we got something for the user: no error here. + // console.warn("expected", this.centralDirRecords, "records in central dir, got", this.files.length); + } + } }, /** * Read the end of central directory. */ readEndOfCentral: function() { var offset = this.reader.lastIndexOfSignature(sig.CENTRAL_DIRECTORY_END); - if (offset === -1) { + if (offset < 0) { // Check if the content is a truncated zip or complete garbage. // A "LOCAL_FILE_HEADER" is not required at the beginning (auto // extractible zip for example) but it can give a good hint. // If an ajax request was used without responseType, we will also // get unreadable data. - var isGarbage = true; - try { - this.reader.setIndex(0); - this.checkSignature(sig.LOCAL_FILE_HEADER); - isGarbage = false; - } catch (e) {} + var isGarbage = !this.isSignature(0, sig.LOCAL_FILE_HEADER); if (isGarbage) { throw new Error("Can't find end of central directory : is this a zip file ? " + - "If it is, see http://stuk.github.io/jszip/documentation/howto/read_zip.html"); + "If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html"); } else { - throw new Error("Corrupted zip : can't find end of central directory"); + throw new Error("Corrupted zip: can't find end of central directory"); } + } this.reader.setIndex(offset); + var endOfCentralDirOffset = offset; this.checkSignature(sig.CENTRAL_DIRECTORY_END); this.readBlockEndOfCentral(); @@ -2437,7 +3745,7 @@ ZipEntries.prototype = { /* Warning : the zip64 extension is supported, but ONLY if the 64bits integer read from - the zip file can fit into a 32bits integer. This cannot be solved : Javascript represents + the zip file can fit into a 32bits integer. This cannot be solved : JavaScript represents all numbers as 64-bit double precision IEEE 754 floating point numbers. So, we have 53bits for integers and bitwise operations treat everything as 32bits. see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators @@ -2446,30 +3754,50 @@ ZipEntries.prototype = { // should look for a zip64 EOCD locator offset = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR); - if (offset === -1) { - throw new Error("Corrupted zip : can't find the ZIP64 end of central directory locator"); + if (offset < 0) { + throw new Error("Corrupted zip: can't find the ZIP64 end of central directory locator"); } this.reader.setIndex(offset); this.checkSignature(sig.ZIP64_CENTRAL_DIRECTORY_LOCATOR); this.readBlockZip64EndOfCentralLocator(); // now the zip64 EOCD record + if (!this.isSignature(this.relativeOffsetEndOfZip64CentralDir, sig.ZIP64_CENTRAL_DIRECTORY_END)) { + // console.warn("ZIP64 end of central directory not where expected."); + this.relativeOffsetEndOfZip64CentralDir = this.reader.lastIndexOfSignature(sig.ZIP64_CENTRAL_DIRECTORY_END); + if (this.relativeOffsetEndOfZip64CentralDir < 0) { + throw new Error("Corrupted zip: can't find the ZIP64 end of central directory"); + } + } this.reader.setIndex(this.relativeOffsetEndOfZip64CentralDir); this.checkSignature(sig.ZIP64_CENTRAL_DIRECTORY_END); this.readBlockZip64EndOfCentral(); } + + var expectedEndOfCentralDirOffset = this.centralDirOffset + this.centralDirSize; + if (this.zip64) { + expectedEndOfCentralDirOffset += 20; // end of central dir 64 locator + expectedEndOfCentralDirOffset += 12 /* should not include the leading 12 bytes */ + this.zip64EndOfCentralSize; + } + + var extraBytes = endOfCentralDirOffset - expectedEndOfCentralDirOffset; + + if (extraBytes > 0) { + // console.warn(extraBytes, "extra bytes at beginning or within zipfile"); + if (this.isSignature(endOfCentralDirOffset, sig.CENTRAL_FILE_HEADER)) { + // The offsets seem wrong, but we have something at the specified offset. + // So… we keep it. + } else { + // the offset is wrong, update the "zero" of the reader + // this happens if data has been prepended (crx files for example) + this.reader.zero = extraBytes; + } + } else if (extraBytes < 0) { + throw new Error("Corrupted zip: missing " + Math.abs(extraBytes) + " bytes."); + } }, prepareReader: function(data) { - var type = utils.getTypeOf(data); - if (type === "string" && !support.uint8array) { - this.reader = new StringReader(data, this.loadOptions.optimizedBinaryString); - } - else if (type === "nodebuffer") { - this.reader = new NodeBufferReader(data); - } - else { - this.reader = new Uint8ArrayReader(utils.transformTo("uint8array", data)); - } + this.reader = readerFor(data); }, /** * Read a zip file and create ZipEntries. @@ -2485,16 +3813,36 @@ ZipEntries.prototype = { // }}} end of ZipEntries module.exports = ZipEntries; -},{"./nodeBufferReader":12,"./object":13,"./signature":14,"./stringReader":15,"./support":17,"./uint8ArrayReader":18,"./utils":21,"./zipEntry":23}],23:[function(_dereq_,module,exports){ -'use strict'; -var StringReader = _dereq_('./stringReader'); -var utils = _dereq_('./utils'); -var CompressedObject = _dereq_('./compressedObject'); -var jszipProto = _dereq_('./object'); +},{"./reader/readerFor":22,"./signature":23,"./support":30,"./utils":32,"./zipEntry":34}],34:[function(require,module,exports){ +"use strict"; +var readerFor = require("./reader/readerFor"); +var utils = require("./utils"); +var CompressedObject = require("./compressedObject"); +var crc32fn = require("./crc32"); +var utf8 = require("./utf8"); +var compressions = require("./compressions"); +var support = require("./support"); var MADE_BY_DOS = 0x00; var MADE_BY_UNIX = 0x03; +/** + * Find a compression registered in JSZip. + * @param {string} compressionMethod the method magic to find. + * @return {Object|null} the JSZip compression object, null if none found. + */ +var findCompression = function(compressionMethod) { + for (var method in compressions) { + if (!Object.prototype.hasOwnProperty.call(compressions, method)) { + continue; + } + if (compressions[method].magic === compressionMethod) { + return compressions[method]; + } + } + return null; +}; + // class ZipEntry {{{ /** * An entry in the zip file. @@ -2523,45 +3871,6 @@ ZipEntry.prototype = { // bit 11 is set return (this.bitFlag & 0x0800) === 0x0800; }, - /** - * Prepare the function used to generate the compressed content from this ZipFile. - * @param {DataReader} reader the reader to use. - * @param {number} from the offset from where we should read the data. - * @param {number} length the length of the data to read. - * @return {Function} the callback to get the compressed content (the type depends of the DataReader class). - */ - prepareCompressedContent: function(reader, from, length) { - return function() { - var previousIndex = reader.index; - reader.setIndex(from); - var compressedFileData = reader.readData(length); - reader.setIndex(previousIndex); - - return compressedFileData; - }; - }, - /** - * Prepare the function used to generate the uncompressed content from this ZipFile. - * @param {DataReader} reader the reader to use. - * @param {number} from the offset from where we should read the data. - * @param {number} length the length of the data to read. - * @param {JSZip.compression} compression the compression used on this file. - * @param {number} uncompressedSize the uncompressed size to expect. - * @return {Function} the callback to get the uncompressed content (the type depends of the DataReader class). - */ - prepareContent: function(reader, from, length, compression, uncompressedSize) { - return function() { - - var compressedFileData = utils.transformTo(compression.uncompressInputType, this.getCompressedContent()); - var uncompressedFileData = compression.uncompress(compressedFileData); - - if (uncompressedFileData.length !== uncompressedSize) { - throw new Error("Bug : uncompressed data size mismatch"); - } - - return uncompressedFileData; - }; - }, /** * Read the local part of a zip file and add the info in this object. * @param {DataReader} reader the reader to use. @@ -2588,32 +3897,19 @@ ZipEntry.prototype = { // Unfortunately, this lead also to some issues : http://seclists.org/fulldisclosure/2009/Sep/394 this.fileNameLength = reader.readInt(2); localExtraFieldsLength = reader.readInt(2); // can't be sure this will be the same as the central dir - this.fileName = reader.readString(this.fileNameLength); + // the fileName is stored as binary data, the handleUTF8 method will take care of the encoding. + this.fileName = reader.readData(this.fileNameLength); reader.skip(localExtraFieldsLength); - if (this.compressedSize == -1 || this.uncompressedSize == -1) { - throw new Error("Bug or corrupted zip : didn't get enough informations from the central directory " + "(compressedSize == -1 || uncompressedSize == -1)"); + if (this.compressedSize === -1 || this.uncompressedSize === -1) { + throw new Error("Bug or corrupted zip : didn't get enough information from the central directory " + "(compressedSize === -1 || uncompressedSize === -1)"); } - compression = utils.findCompression(this.compressionMethod); + compression = findCompression(this.compressionMethod); if (compression === null) { // no compression found - throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + this.fileName + ")"); - } - this.decompressed = new CompressedObject(); - this.decompressed.compressedSize = this.compressedSize; - this.decompressed.uncompressedSize = this.uncompressedSize; - this.decompressed.crc32 = this.crc32; - this.decompressed.compressionMethod = this.compressionMethod; - this.decompressed.getCompressedContent = this.prepareCompressedContent(reader, reader.index, this.compressedSize, compression); - this.decompressed.getContent = this.prepareContent(reader, reader.index, this.compressedSize, compression, this.uncompressedSize); - - // we need to compute the crc32... - if (this.loadOptions.checkCRC32) { - this.decompressed = utils.transformTo("string", this.decompressed.getContent()); - if (jszipProto.crc32(this.decompressed) !== this.crc32) { - throw new Error("Corrupted zip : CRC32 mismatch"); - } + throw new Error("Corrupted zip : compression " + utils.pretty(this.compressionMethod) + " unknown (inner file : " + utils.transformTo("string", this.fileName) + ")"); } + this.decompressed = new CompressedObject(this.compressedSize, this.uncompressedSize, this.crc32, compression, reader.readData(this.compressedSize)); }, /** @@ -2622,14 +3918,15 @@ ZipEntry.prototype = { */ readCentralPart: function(reader) { this.versionMadeBy = reader.readInt(2); - this.versionNeeded = reader.readInt(2); + reader.skip(2); + // this.versionNeeded = reader.readInt(2); this.bitFlag = reader.readInt(2); this.compressionMethod = reader.readString(2); this.date = reader.readDate(); this.crc32 = reader.readInt(4); this.compressedSize = reader.readInt(4); this.uncompressedSize = reader.readInt(4); - this.fileNameLength = reader.readInt(2); + var fileNameLength = reader.readInt(2); this.extraFieldsLength = reader.readInt(2); this.fileCommentLength = reader.readInt(2); this.diskNumberStart = reader.readInt(2); @@ -2641,10 +3938,11 @@ ZipEntry.prototype = { throw new Error("Encrypted zip are not supported"); } - this.fileName = reader.readString(this.fileNameLength); + // will be read in the local part, see the comments there + reader.skip(fileNameLength); this.readExtraFields(reader); this.parseZIP64ExtraField(reader); - this.fileComment = reader.readString(this.fileCommentLength); + this.fileComment = reader.readData(this.fileCommentLength); }, /** @@ -2671,7 +3969,7 @@ ZipEntry.prototype = { } // fail safe : if the name ends with a / it probably means a folder - if (!this.dir && this.fileName.slice(-1) === '/') { + if (!this.dir && this.fileNameStr.slice(-1) === "/") { this.dir = true; } }, @@ -2680,14 +3978,13 @@ ZipEntry.prototype = { * Parse the ZIP64 extra field and merge the info in the current ZipEntry. * @param {DataReader} reader the reader to use. */ - parseZIP64ExtraField: function(reader) { - + parseZIP64ExtraField: function() { if (!this.extraFields[0x0001]) { return; } // should be something, preparing the extra reader - var extraReader = new StringReader(this.extraFields[0x0001].value); + var extraReader = readerFor(this.extraFields[0x0001].value); // I really hope that these 64bits integer can fit in 32 bits integer, because js // won't let us have more. @@ -2709,17 +4006,19 @@ ZipEntry.prototype = { * @param {DataReader} reader the reader to use. */ readExtraFields: function(reader) { - var start = reader.index, + var end = reader.index + this.extraFieldsLength, extraFieldId, extraFieldLength, extraFieldValue; - this.extraFields = this.extraFields || {}; + if (!this.extraFields) { + this.extraFields = {}; + } - while (reader.index < start + this.extraFieldsLength) { + while (reader.index + 4 < end) { extraFieldId = reader.readInt(2); extraFieldLength = reader.readInt(2); - extraFieldValue = reader.readString(extraFieldLength); + extraFieldValue = reader.readData(extraFieldLength); this.extraFields[extraFieldId] = { id: extraFieldId, @@ -2727,22 +4026,34 @@ ZipEntry.prototype = { value: extraFieldValue }; } + + reader.setIndex(end); }, /** * Apply an UTF8 transformation if needed. */ handleUTF8: function() { + var decodeParamType = support.uint8array ? "uint8array" : "array"; if (this.useUTF8()) { - this.fileName = jszipProto.utf8decode(this.fileName); - this.fileComment = jszipProto.utf8decode(this.fileComment); + this.fileNameStr = utf8.utf8decode(this.fileName); + this.fileCommentStr = utf8.utf8decode(this.fileComment); } else { var upath = this.findExtraFieldUnicodePath(); if (upath !== null) { - this.fileName = upath; + this.fileNameStr = upath; + } else { + // ASCII text or unsupported code page + var fileNameByteArray = utils.transformTo(decodeParamType, this.fileName); + this.fileNameStr = this.loadOptions.decodeFileName(fileNameByteArray); } + var ucomment = this.findExtraFieldUnicodeComment(); if (ucomment !== null) { - this.fileComment = ucomment; + this.fileCommentStr = ucomment; + } else { + // ASCII text or unsupported code page + var commentByteArray = utils.transformTo(decodeParamType, this.fileComment); + this.fileCommentStr = this.loadOptions.decodeFileName(commentByteArray); } } }, @@ -2754,7 +4065,7 @@ ZipEntry.prototype = { findExtraFieldUnicodePath: function() { var upathField = this.extraFields[0x7075]; if (upathField) { - var extraReader = new StringReader(upathField.value); + var extraReader = readerFor(upathField.value); // wrong version if (extraReader.readInt(1) !== 1) { @@ -2762,11 +4073,11 @@ ZipEntry.prototype = { } // the crc of the filename changed, this field is out of date. - if (jszipProto.crc32(this.fileName) !== extraReader.readInt(4)) { + if (crc32fn(this.fileName) !== extraReader.readInt(4)) { return null; } - return jszipProto.utf8decode(extraReader.readString(upathField.length - 5)); + return utf8.utf8decode(extraReader.readData(upathField.length - 5)); } return null; }, @@ -2778,7 +4089,7 @@ ZipEntry.prototype = { findExtraFieldUnicodeComment: function() { var ucommentField = this.extraFields[0x6375]; if (ucommentField) { - var extraReader = new StringReader(ucommentField.value); + var extraReader = readerFor(ucommentField.value); // wrong version if (extraReader.readInt(1) !== 1) { @@ -2786,42 +4097,527 @@ ZipEntry.prototype = { } // the crc of the comment changed, this field is out of date. - if (jszipProto.crc32(this.fileComment) !== extraReader.readInt(4)) { + if (crc32fn(this.fileComment) !== extraReader.readInt(4)) { return null; } - return jszipProto.utf8decode(extraReader.readString(ucommentField.length - 5)); + return utf8.utf8decode(extraReader.readData(ucommentField.length - 5)); } return null; } }; module.exports = ZipEntry; -},{"./compressedObject":2,"./object":13,"./stringReader":15,"./utils":21}],24:[function(_dereq_,module,exports){ +},{"./compressedObject":2,"./compressions":3,"./crc32":4,"./reader/readerFor":22,"./support":30,"./utf8":31,"./utils":32}],35:[function(require,module,exports){ +"use strict"; + +var StreamHelper = require("./stream/StreamHelper"); +var DataWorker = require("./stream/DataWorker"); +var utf8 = require("./utf8"); +var CompressedObject = require("./compressedObject"); +var GenericWorker = require("./stream/GenericWorker"); + +/** + * A simple object representing a file in the zip file. + * @constructor + * @param {string} name the name of the file + * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data + * @param {Object} options the options of the file + */ +var ZipObject = function(name, data, options) { + this.name = name; + this.dir = options.dir; + this.date = options.date; + this.comment = options.comment; + this.unixPermissions = options.unixPermissions; + this.dosPermissions = options.dosPermissions; + + this._data = data; + this._dataBinary = options.binary; + // keep only the compression + this.options = { + compression : options.compression, + compressionOptions : options.compressionOptions + }; +}; + +ZipObject.prototype = { + /** + * Create an internal stream for the content of this object. + * @param {String} type the type of each chunk. + * @return StreamHelper the stream. + */ + internalStream: function (type) { + var result = null, outputType = "string"; + try { + if (!type) { + throw new Error("No output type specified."); + } + outputType = type.toLowerCase(); + var askUnicodeString = outputType === "string" || outputType === "text"; + if (outputType === "binarystring" || outputType === "text") { + outputType = "string"; + } + result = this._decompressWorker(); + + var isUnicodeString = !this._dataBinary; + + if (isUnicodeString && !askUnicodeString) { + result = result.pipe(new utf8.Utf8EncodeWorker()); + } + if (!isUnicodeString && askUnicodeString) { + result = result.pipe(new utf8.Utf8DecodeWorker()); + } + } catch (e) { + result = new GenericWorker("error"); + result.error(e); + } + + return new StreamHelper(result, outputType, ""); + }, + + /** + * Prepare the content in the asked type. + * @param {String} type the type of the result. + * @param {Function} onUpdate a function to call on each internal update. + * @return Promise the promise of the result. + */ + async: function (type, onUpdate) { + return this.internalStream(type).accumulate(onUpdate); + }, + + /** + * Prepare the content as a nodejs stream. + * @param {String} type the type of each chunk. + * @param {Function} onUpdate a function to call on each internal update. + * @return Stream the stream. + */ + nodeStream: function (type, onUpdate) { + return this.internalStream(type || "nodebuffer").toNodejsStream(onUpdate); + }, + + /** + * Return a worker for the compressed content. + * @private + * @param {Object} compression the compression object to use. + * @param {Object} compressionOptions the options to use when compressing. + * @return Worker the worker. + */ + _compressWorker: function (compression, compressionOptions) { + if ( + this._data instanceof CompressedObject && + this._data.compression.magic === compression.magic + ) { + return this._data.getCompressedWorker(); + } else { + var result = this._decompressWorker(); + if(!this._dataBinary) { + result = result.pipe(new utf8.Utf8EncodeWorker()); + } + return CompressedObject.createWorkerFrom(result, compression, compressionOptions); + } + }, + /** + * Return a worker for the decompressed content. + * @private + * @return Worker the worker. + */ + _decompressWorker : function () { + if (this._data instanceof CompressedObject) { + return this._data.getContentWorker(); + } else if (this._data instanceof GenericWorker) { + return this._data; + } else { + return new DataWorker(this._data); + } + } +}; + +var removedMethods = ["asText", "asBinary", "asNodeBuffer", "asUint8Array", "asArrayBuffer"]; +var removedFn = function () { + throw new Error("This method has been removed in JSZip 3.0, please check the upgrade guide."); +}; + +for(var i = 0; i < removedMethods.length; i++) { + ZipObject.prototype[removedMethods[i]] = removedFn; +} +module.exports = ZipObject; + +},{"./compressedObject":2,"./stream/DataWorker":27,"./stream/GenericWorker":28,"./stream/StreamHelper":29,"./utf8":31}],36:[function(require,module,exports){ +(function (global){ +'use strict'; +var Mutation = global.MutationObserver || global.WebKitMutationObserver; + +var scheduleDrain; + +{ + if (Mutation) { + var called = 0; + var observer = new Mutation(nextTick); + var element = global.document.createTextNode(''); + observer.observe(element, { + characterData: true + }); + scheduleDrain = function () { + element.data = (called = ++called % 2); + }; + } else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined') { + var channel = new global.MessageChannel(); + channel.port1.onmessage = nextTick; + scheduleDrain = function () { + channel.port2.postMessage(0); + }; + } else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) { + scheduleDrain = function () { + + // Create a + + + + \ No newline at end of file diff --git a/Layout/default/TimerecordingCategories/Form.php b/Layout/default/TimerecordingCategories/Form.php new file mode 100644 index 000000000..f7ca11247 --- /dev/null +++ b/Layout/default/TimerecordingCategories/Form.php @@ -0,0 +1,117 @@ + + + + +
+
+
+
+ +
+

id) ? "Zeiterfassung Buchungsart bearbeiten" : "Neue Zeiterfassung Buchungsart" ?>

+
+
+
+ +
+
+ +
+
+

id) ? "Zeiterfassung Buchungsart bearbeiten" : "Neue Zeiterfassung Buchungsart" ?>

+ +
"> +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+
+ approval) echo 'checked="checked"'; ?> + type="checkbox" name="approval" value="1" id="olt"> +
+
+
+
+ +
+
+ require_comment) echo 'checked="checked"'; ?> + type="checkbox" name="require_comment" value="1" id="olt"> +
+
+
+
+
+
+ +
+ + "> + + +
+
+ + +
+
+
+ +
+
+ + + + \ No newline at end of file diff --git a/Layout/default/TimerecordingCategories/Index.php b/Layout/default/TimerecordingCategories/Index.php new file mode 100644 index 000000000..37747edcf --- /dev/null +++ b/Layout/default/TimerecordingCategories/Index.php @@ -0,0 +1,93 @@ + + + + +
+
+
+
+ +
+

Zeiterfassung Buchungsarten

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

Liste aller Zeiterfassung Buchungsarten

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeizeichnungBMD KZBuchungszeitraumGenehmigungspflichtigAnmerkung Pflichtfeld
name ?>short ?>hourday] ?>approval] ?>require_comment] ?> + $timerecordingcategories->id]) ?>"> + $timerecordingcategories->id]) ?>" + onclick="if(!confirm('Zeiterfassung Kategorie wirklich löschen?')) return false;" class="text-danger" + title="Löschen"> +
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Layout/default/TimerecordingPermit/Index.php b/Layout/default/TimerecordingPermit/Index.php new file mode 100644 index 000000000..ad5045ded --- /dev/null +++ b/Layout/default/TimerecordingPermit/Index.php @@ -0,0 +1,290 @@ + + + + + + +
+
+
+
+ +
+

Buchungen

+
+
+
+ +
+
+
+
+
+

Liste aller Buchungen

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + start; + if ($timerecording->timerecordingCategory->hourday == 1) { + $date = date("d.m.Y", $timerecording->start); + $datadate = date("Y-m-d", $timerecording->start); + $start = date("H:i", $timerecording->start); + $end = date("H:i", $timerecording->end); + $seconds = $timerecording->end - $timerecording->start; + $minutes = floor(($seconds % 3600) / 60); + $hours = floor($seconds / 3600); + $sum = sprintf("%02d", $hours) . ":" . sprintf("%02d", $minutes); + $day = $daysgerm[date("w", $timerecording->start)]; + } else if ($timerecording->timerecordingCategory->hourday == 2) { + $date = date("d.m.", $timerecording->start) . " - " . $daysgerm[date("w", $timerecording->end)] . " " . date("d.m.Y", $timerecording->end); + $datadate = date("Y-m-d", $timerecording->start); + $enddate = date("Y-m-d", $timerecording->end); + $start = "-"; + $end = "-"; + $day=$daysgerm[date("w", $timerecording->start)]; + } else if ($timerecording->timerecordingCategory->hourday == 3 || $timerecording->timerecordingCategory->hourday == 4) { + $date = date("d.m.Y", $timerecording->start); + $datadate = date("Y-m-d", $timerecording->start); + $start = "-"; + $end = "-"; + $day = $daysgerm[date("w", $timerecording->start)]; + } + + if ($timerecording->timerecordingCategory->approval == 1 && $timerecording->approved == 0) { + $state = ''; + } else if ($timerecording->timerecordingCategory->approval == 1 && $timerecording->approved == 1) { + $state = ''; + } + $approved = 'Offen'; + if ($timerecording->approved == 1) $approved = 'Genehmigt'; + $completed = 'Genehmigt'; +// if ($timerecording->completed == 1) $completed = 'Genehmigt'; + ?> + + + + + + + + + + + + + +
DatumMitarbeiterVonBisSummeBuchungsartAnmerkungFreigabe
user->name ?>timerecordingCategory->name ?>commend ?> + completed == 0): + if ($timerecording->approved == 0) : ?> + $timerecording->id]) ?>" + onclick="if(!confirm('Buchung genehmigen?')) return false;"> + $timerecording->id]) ?>" + onclick="if(!confirm('Buchung wirklich ablehnen?')) return false;" + class="text-danger" + title="Ablehnen"> + +
+ + id + ?> + + timerecordingCategory->id + ?> + + + + + $timerecording->id]) + ?> + + + +
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Layout/default/TimerecordingReport/Index.php b/Layout/default/TimerecordingReport/Index.php new file mode 100644 index 000000000..5eaac5630 --- /dev/null +++ b/Layout/default/TimerecordingReport/Index.php @@ -0,0 +1,264 @@ + + + + + +
+
+
+
+ +
+

Buchungen

+
+
+
+ +
+
+
+
+
+

Liste aller Buchungen

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + start; + if ($timerecording->timerecordingCategory->hourday == 1) { + $date = date("d.m.Y", $timerecording->start); + $datadate = date("Y-m-d", $timerecording->start); + $start = date("H:i", $timerecording->start); + $end = date("H:i", $timerecording->end); + $seconds = $timerecording->end - $timerecording->start; + $minutes = floor(($seconds % 3600) / 60); + $hours = floor($seconds / 3600); + $sum = sprintf("%02d", $hours) . ":" . sprintf("%02d", $minutes); + $day = $daysgerm[date("w", $timerecording->start)]; + } else if ($timerecording->timerecordingCategory->hourday == 2) { + $date = date("d.m.", $timerecording->start) . " - " . $daysgerm[date("w", $timerecording->end)] . " " . date("d.m.Y", $timerecording->end); + $datadate = date("Y-m-d", $timerecording->start); + $enddate = date("Y-m-d", $timerecording->end); + $start = "-"; + $end = "-"; + $day = $daysgerm[date("w", $timerecording->start)]; + } else if ($timerecording->timerecordingCategory->hourday == 3 || $timerecording->timerecordingCategory->hourday == 4) { + $date = date("d.m.Y", $timerecording->start); + $datadate = date("Y-m-d", $timerecording->start); + $start = "-"; + $end = "-"; + $day = $daysgerm[date("w", $timerecording->start)]; + } + + if ($timerecording->timerecordingCategory->approval == 1 && $timerecording->approved == 0) { + $state = ''; + } else if ($timerecording->timerecordingCategory->approval == 1 && $timerecording->approved == 1) { + $state = ''; + } + $completed = 'Nein'; + if ($timerecording->completed == 1) $completed = 'Ja'; + + ?> + + + + + + + + + + +completed == 0): +// if ($timerecording->approved == 0) : ?> +id ?> + +timerecordingCategory->id ?> + + + + + + + + $timerecording->id]) ?> + + + + + + + +
DatumMitarbeiterVonBisSummeBuchungsartAnmerkungVerbucht
user->name ?>timerecordingCategory->name ?>commend ?>
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/Layout/default/menu.php b/Layout/default/menu.php index 137750cc2..831cb3181 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -29,7 +29,18 @@ - + is("employee")): ?> +
  • Zeiterfassung
    + +
  • + is(["Admin"]) || ($me->is("netowner") && $me->hasGwrNetworks())): ?>
  • @@ -54,7 +65,7 @@ is(["Admin"]) || ($me->is("netowner") && $me->hasGwrNetworks())): ?>
  • "> GWR / AddressDB
  • - + is(["Admin"])): ?>
  • "> Open Access IDs
  • "> Technologien
  • @@ -64,8 +75,8 @@ - - + + is(["Admin","netowner","lineplanner","pipeplanner","pipeworker","lineworker"])): ?>
  • @@ -104,7 +115,7 @@
  • - + is(["Admin","netowner","salespartner"]) || $me->can(["Order", "Preorder"])): ?>
  • @@ -134,7 +145,7 @@
  • --> - + diff --git a/application/Timerecording/Timerecording.php b/application/Timerecording/Timerecording.php new file mode 100644 index 000000000..86c9aeade --- /dev/null +++ b/application/Timerecording/Timerecording.php @@ -0,0 +1,61 @@ +$name == null) { + + if (!$this->id) { + return null; + } + + if($name == "creator") { + $this->creator = mfValuecache::singleton()->get("Worker-id-".$this->create_by); + if($this->creator === null) { + $this->creator = new User($this->create_by); + if($this->creator->id) { + mfValuecache::singleton()->set("Worker-id-".$this->create_by, $this->creator); + } + } + return $this->creator; + } + + if($name == "editor") { + $this->editor = mfValuecache::singleton()->get("Worker-id-".$this->edit_by); + if($this->editor === null) { + $this->editor = new User($this->edit_by); + if($this->editor->id) { + mfValuecache::singleton()->set("Worker-id-".$this->edit_by, $this->editor); + } + } + return $this->editor; + } + + $classname = ucfirst($name); + $idfield = $name."_id"; + $this->$name = mfValuecache::singleton()->get("mfObjectmodel-$name-".$this->$idfield); + if(!$this->$name) { + $this->$name = new $classname($this->$idfield); + } + + if($this->$name->id) { + mfValuecache::singleton()->set("mfObjectmodel-$name-".$this->$name->id, $this->$name); + return $this->$name; + } else { + return null; + } + + } + + return $this->$name; + } + +} \ No newline at end of file diff --git a/application/Timerecording/TimerecordingController.php b/application/Timerecording/TimerecordingController.php new file mode 100644 index 000000000..0996afd98 --- /dev/null +++ b/application/Timerecording/TimerecordingController.php @@ -0,0 +1,145 @@ +needlogin = true; + $me = new User(); + $me->loadMe(); + $this->me = $me; + $this->layout()->set("me", $me); + + if (!$me->is(["employee"])) { + $this->redirect("Dashboard"); + } + } + + protected function indexAction() + { + $this->layout()->setTemplate("Timerecording/Index"); + $timerecordingCategoriess = TimerecordingCategoryModel::getAll(); + $this->layout()->set("timerecordingCategoriess", $timerecordingCategoriess); + $timerecordings = TimerecordingModel::search(['user_id' => $this->me->id]); + $this->layout()->set("timerecordings", $timerecordings); + } + + protected function addAction() + { + $timerecordingCategoriess = TimerecordingCategoryModel::getAll(); + $this->layout()->set("timerecordingCategoriess", $timerecordingCategoriess); + $this->layout()->setTemplate("Timerecording/Form"); + } + + protected function editAction() + { + $id = $this->request->id; + + if (!is_numeric($id) || !$id) { + $this->layout()->setFlash("Buchung nicht gefunden", "error"); + $this->redirect("Timerecording"); + } + + $timerecordings = new Timerecording($id); + if ($timerecordings->id != $id) { + $this->layout()->setFlash("Buchung nicht gefunden", "error"); + $this->redirect("Timerecording"); + } + + + $this->layout()->set("timerecordings", $timerecordings); + return $this->addAction(); + } + + protected function saveAction() + { + $r = $this->request; + $id = $r->id; + $enddate = $r->enddate; + if (!$enddate && $r->start && $r->end) { + $starttime = strtotime($r->date . " " . $r->start . ":00"); + $endtime = strtotime($r->date . " " . $r->end . ":00"); + } elseif ($enddate) { + $starttime = strtotime($r->date . " 00:00:00"); + $endtime = strtotime($enddate . " 23:59:00"); + } else if (!$enddate && !$r->start && !$r->end) { + $starttime = strtotime($r->date . " 00:00:00"); + $endtime = NULL; + } + + if (is_numeric($id) && $id > 0) { + $mode = "edit"; + $timerecordings = new Timerecording($id); + if (!$timerecordings->id) { + $this->layout()->setFlash("Buchungen nicht gefunden", "error"); + $this->redirect("Timerecording"); + } + } else { + $mode = "add"; + } + + $data = []; + $data['user_id'] = $this->me->id; + $data['start'] = $starttime; + $data['end'] = $endtime; + $data['timerecordingCategory_id'] = trim($r->timerecordingCategory_id); + $data['commend'] = trim($r->commend); + + if (!$data['user_id']) { + $this->layout()->setFlash("Benutzer darf nicht leer sein", "error"); + $this->redirect("Timerecording"); + } + if ($data['start'] < 1577833200) { + $this->layout()->setFlash("Ungültige Startzeit", "error"); + $this->redirect("Timerecording"); + } + if ($data['end'] && $data['end'] < 1577833200) { + $this->layout()->setFlash("Ungültige Endzeit", "error"); + $this->redirect("Timerecording"); + } + if (!$data['timerecordingCategory_id']) { + $data['timerecordingCategory_id'] = NULL; + } + if (!$data['commend']) { + $data['commend'] = NULL; + } + + if ($mode == "edit") { + $timerecordings->update($data); + + } else { + $timerecordings = TimerecordingModel::create($data); + } +// var_dump($filestore); +// exit; + $id = $timerecordings->save(); + + if (!$id) { + $this->layout()->setFlash("Buchung konnte nicht angelegt werden", "error"); + $this->redirect("Timerecording"); + } + + if ($mode == "edit") { + $this->layout()->setFlash("Buchung erfolgreich geändert", "success"); + } else if ($mode = "add") { + $this->layout()->setFlash("Buchung erfolgreich angelegt", "success"); + } + $this->redirect("Timerecording"); + } + + + protected function deleteAction() + { + $id = $this->request->id; + $timerecordings = new Timerecording($id); + if (!$timerecordings->id || $timerecordings->id != $id) { + $this->layout()->setFlash("Buchung nicht gefunden.", "error"); + $this->redirect("Timerecording"); + } + + $timerecordings->delete(); + $this->redirect("Timerecording"); + } + +} diff --git a/application/Timerecording/TimerecordingModel.php b/application/Timerecording/TimerecordingModel.php new file mode 100644 index 000000000..f8b6b8039 --- /dev/null +++ b/application/Timerecording/TimerecordingModel.php @@ -0,0 +1,148 @@ + $value) { + if (property_exists(get_called_class(), $field)) { + if (substr($field, 0, 5) == "vlan_" && !$value) { + $model->$field = null; + continue; + } + $model->$field = $value; + } + } + + $me = mfValuecache::singleton()->get("me"); + if (!$me) { + $me = new User(); + $me->loadMe(); + mfValuecache::singleton()->set("me", $me); + } + + if ($model->create_by === null) { + $model->create_by = $me->id; + } + if ($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getOne($id) + { + if (!is_numeric($id) || !$id) { + throw new Exception("Invalid number", 400); + } + $item = []; + $db = FronkDB::singleton(); + + $res = $db->select("Timerecording", "*", "id=$id LIMIT 1"); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new Timerecording($data); + } + return $item; + } + + public static function getAll() + { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("Timerecording", "*", "1=1"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new Timerecording($data); + } + } + return $items; + + } + public static function getAllPermits() + { + $items = []; + + $db = FronkDB::singleton(); + + $sql = "SELECT Timerecording.* FROM `Timerecording` + INNER JOIN `TimerecordingCategory` ON (`Timerecording`.`timerecordingCategory_id` = `TimerecordingCategory`.`id`) + WHERE `TimerecordingCategory`.`approval`='1'"; + + $res = $db->query($sql); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new Timerecording($data); + } + } + return $items; + + } + + public static function getFirst() + { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("Timerecording", "*", "$where "); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new Timerecording($data); + if ($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function search($filter) + { + $items = []; + $db = FronkDB::singleton(); + $where = self::getSqlFilter($filter); + $res = $db->select("Timerecording", "*", "$where"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new Timerecording($data); + } + } + return $items; + } + + private static function getSqlFilter($filter) + { + $where = "1=1 "; + + if (array_key_exists("user_id", $filter)) { + $userid = $filter['user_id']; + if (is_numeric($userid)) { + $where .= " AND user_id=$userid"; + } + } + + //var_dump($filter, $where);exit; + return $where; + } + +} diff --git a/application/TimerecordingCategory/TimerecordingCategory.php b/application/TimerecordingCategory/TimerecordingCategory.php new file mode 100644 index 000000000..c3166ba7c --- /dev/null +++ b/application/TimerecordingCategory/TimerecordingCategory.php @@ -0,0 +1,41 @@ +$name == null) { + + if (!$this->id) { + return null; + } + + if ($name == "creator") { + $this->creator = new User($this->create_by); + return $this->creator; + } + + if ($name == "editor") { + $this->editor = new User($this->edit_by); + return $this->editor; + } + + $classname = ucfirst($name); + $idfield = $name . "_id"; + $this->$name = new $classname($this->$idfield); + + if ($this->$name->id) { + return $this->$name; + } else { + return null; + } + } + + return $this->$name; + } + +} \ No newline at end of file diff --git a/application/TimerecordingCategory/TimerecordingCategoryController.php b/application/TimerecordingCategory/TimerecordingCategoryController.php new file mode 100644 index 000000000..7237ea75c --- /dev/null +++ b/application/TimerecordingCategory/TimerecordingCategoryController.php @@ -0,0 +1,141 @@ +needlogin = true; + $me = new User(); + $me->loadMe(); + $this->me = $me; + $this->layout()->set("me", $me); + + if (!$me->can(["Fibu"])) { + $this->redirect("Dashboard"); + } + } + + protected function indexAction() + { + + $this->layout()->setTemplate("TimerecordingCategories/Index"); + $timerecordingcategoriesapproval = TimerecordingCategoryModel::$approval_definition; + $timerecordingcategorieshourday = TimerecordingCategoryModel::$hourday_definition; + $timerecordingcategoriesrequire_comment = TimerecordingCategoryModel::$require_comment_definition; + $timerecordingcategoriess = TimerecordingCategoryModel::getAll(); + $this->layout()->set("timerecordingcategoriesrequire_comment", $timerecordingcategoriesrequire_comment); + $this->layout()->set("timerecordingcategorieshourday", $timerecordingcategorieshourday); + $this->layout()->set("timerecordingcategoriesapproval", $timerecordingcategoriesapproval); + $this->layout()->set("timerecordingcategoriess", $timerecordingcategoriess); + + } + + protected function addAction() + { + $timerecordingcategorieshourday = TimerecordingCategoryModel::$hourday_definition; + $this->layout()->set("timerecordingcategorieshourday", $timerecordingcategorieshourday); + $this->layout()->setTemplate("TimerecordingCategories/Form"); + + } + + protected function editAction() + { + $id = $this->request->id; + + if (!is_numeric($id) || !$id) { + $this->layout()->setFlash("Zeiterfassung Kategorie nicht gefunden", "error"); + $this->redirect("TimerecordingCategory"); + } + + $timerecordingcategoriess = new TimerecordingCategory($id); + if ($timerecordingcategoriess->id != $id) { + $this->layout()->setFlash("Zeiterfassung Kategorie nicht gefunden", "error"); + $this->redirect("TimerecordingCategory"); + } + $timerecordingcategorieshourday = TimerecordingCategoryModel::$hourday_definition; + $this->layout()->set("timerecordingcategorieshourday", $timerecordingcategorieshourday); + $this->layout()->set("timerecordingcategoriess", $timerecordingcategoriess); + return $this->addAction(); + } + + protected function saveAction() + { + $r = $this->request; + $id = $r->id; + //var_dump($r->get());exit; + if (is_numeric($id) && $id > 0) { + $mode = "edit"; + $timerecordingcategoriess = new TimerecordingCategory($id); + if (!$timerecordingcategoriess->id) { + $this->layout()->setFlash("Zeiterfassung Kategorien nicht gefunden", "error"); + $this->redirect("TimerecordingCategory"); + } + } else { + $mode = "add"; + } + + $data = []; + $data['name'] = trim($r->name); + $data['short'] = trim($r->short); + $data['hourday'] = trim($r->hourday); + $data['approval'] = trim($r->approval); + $data['require_comment'] = trim($r->require_comment); + + + if (!$data['name']) { + $data['name'] = NULL; + } + if (!$data['short']) { + $data['short'] = NULL; + } + if (!$data['approval']) { + $data['approval'] = 0; + } + if (!$data['require_comment']) { + $data['require_comment'] = 0; + } + +// var_dump($_FILES); +// var_dump($upload); +// exit; + + + if ($mode == "edit") { + $timerecordingcategoriess->update($data); + + } else { + $timerecordingcategoriess = TimerecordingCategoryModel::create($data); + } +// var_dump($filestore); +// exit; + $id = $timerecordingcategoriess->save(); + + if (!$id) { + $this->layout()->setFlash("Zeiterfassung Kategorie konnte nicht angelegt werden", "error"); + $this->redirect("TimerecordingCategory"); + } + + if ($mode == "edit") { + $this->layout()->setFlash("Zeiterfassung Kategorie erfolgreich geändert", "success"); + } else if ($mode = "add") { + $this->layout()->setFlash("Zeiterfassung Kategorie erfolgreich angelegt", "success"); + } + $this->redirect("TimerecordingCategory"); + } + + + protected function deleteAction() + { + $id = $this->request->id; + $timerecordingcategoriess = new TimerecordingCategory($id); + if (!$timerecordingcategoriess->id || $timerecordingcategoriess->id != $id) { + $this->layout()->setFlash("Zeiterfassung Kategorie nicht gefunden.", "error"); + $this->redirect("TimerecordingCategory"); + } + + $timerecordingcategoriess->delete(); + $this->redirect("TimerecordingCategory"); + } + +} diff --git a/application/TimerecordingCategory/TimerecordingCategoryModel.php b/application/TimerecordingCategory/TimerecordingCategoryModel.php new file mode 100644 index 000000000..88aedcecb --- /dev/null +++ b/application/TimerecordingCategory/TimerecordingCategoryModel.php @@ -0,0 +1,132 @@ + "Uhrzeit (von/bis)", 2 => "Tage (von/bis)", 3 => "Startdatum", 4 => "Enddatum"); + public static $approval_definition = array(0 => "Nein", 1 => "Ja"); + public static $require_comment_definition = array(0 => "Nein", 1 => "Ja"); + + public static function find($data) + { + + } + + public static function create(array $data) + { + $model = new TimerecordingCategory(); + + foreach ($data as $field => $value) { + if (property_exists(get_called_class(), $field)) { + if (substr($field, 0, 5) == "vlan_" && !$value) { + $model->$field = null; + continue; + } + $model->$field = $value; + } + } + + $me = mfValuecache::singleton()->get("me"); + if (!$me) { + $me = new User(); + $me->loadMe(); + mfValuecache::singleton()->set("me", $me); + } + + if ($model->create_by === null) { + $model->create_by = $me->id; + } + if ($model->edit_by === null) { + $model->edit_by = $me->id; + } + + return $model; + } + + public static function getOne($id) + { + if (!is_numeric($id) || !$id) { + throw new Exception("Invalid number", 400); + } + $item = []; + $db = FronkDB::singleton(); + + $res = $db->select("timerecordingCategory", "*", "id=$id LIMIT 1"); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new TimerecordingCategory($data); + } + return $item; + } + + public static function getAll() + { + $items = []; + + $db = FronkDB::singleton(); + + $res = $db->select("TimerecordingCategory", "*", "1=1"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new TimerecordingCategory($data); + } + } + return $items; + + } + + + public static function getFirst() + { + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("TimerecordingCategory", "*", "$where "); + if ($db->num_rows($res)) { + $data = $db->fetch_object($res); + $item = new TimerecordingCategory($data); + if ($item->id) { + return $item; + } else { + return null; + } + } + return null; + } + + public static function search($filter) + { + $items = []; + $db = FronkDB::singleton(); + + $where = self::getSqlFilter($filter); + $res = $db->select("TimerecordingCategory", "*", "$where"); + if ($db->num_rows($res)) { + while ($data = $db->fetch_object($res)) { + $items[] = new TimerecordingCategory($data); + } + } + return $items; + } + + private static function getSqlFilter($filter) + { + $where = "1=1 "; + + //var_dump($filter);exit; + if (array_key_exists("network_id", $filter)) { + $networkid = $filter['network_id']; + if (is_numeric($networkid)) { + $where .= " AND network_id=$networkid"; + } + } + + //var_dump($filter, $where);exit; + return $where; + } + +} diff --git a/application/TimerecordingPermit/TimerecordingPermitController.php b/application/TimerecordingPermit/TimerecordingPermitController.php new file mode 100644 index 000000000..6b9025d78 --- /dev/null +++ b/application/TimerecordingPermit/TimerecordingPermitController.php @@ -0,0 +1,62 @@ +needlogin = true; + $me = new User(); + $me->loadMe(); + $this->me = $me; + $this->layout()->set("me", $me); + + if (!$me->can(["Fibu"])) { + $this->redirect("Dashboard"); + } + } + + protected function indexAction() + { + + $this->layout()->setTemplate("TimerecordingPermit/Index"); + $timerecordingCategoriess = TimerecordingCategoryModel::getAll(); + $this->layout()->set("timerecordingCategoriess", $timerecordingCategoriess); + $timerecordings = TimerecordingModel::getAllPermits(); + $this->layout()->set("timerecordings", $timerecordings); + + } + + protected function addAction() + { + + + } + + protected function editAction() + { + + } + + protected function saveAction() + { + } + + + protected function approveAction() + { + $id = $this->request->id; + $timerecordings = new Timerecording($id); + if (!$timerecordings->id || $timerecordings->id != $id) { + $this->layout()->setFlash("Buchung nicht gefunden.", "error"); + $this->redirect("TimerecordingPermit"); + } + $data = []; + $data['approved'] = 1; + $timerecordings->update($data); + $timerecordings->save(); + + $this->redirect("TimerecordingPermit"); + } + +} diff --git a/application/TimerecordingReport/TimerecordingReportController.php b/application/TimerecordingReport/TimerecordingReportController.php new file mode 100644 index 000000000..6d5396b79 --- /dev/null +++ b/application/TimerecordingReport/TimerecordingReportController.php @@ -0,0 +1,51 @@ +needlogin = true; + $me = new User(); + $me->loadMe(); + $this->me = $me; + $this->layout()->set("me", $me); + + if (!$me->can(["Fibu"])) { + $this->redirect("Dashboard"); + } + } + + protected function indexAction() + { + + $this->layout()->setTemplate("TimerecordingReport/Index"); + $timerecordingCategoriess = TimerecordingCategoryModel::getAll(); + $this->layout()->set("timerecordingCategoriess", $timerecordingCategoriess); + $timerecordings = TimerecordingModel::getAll(); + $this->layout()->set("timerecordings", $timerecordings); + + } + + protected function addAction() + { + + + } + + protected function editAction() + { + + } + + protected function saveAction() + { + } + + + protected function deleteAction() + { + + } + +} diff --git a/db/migrations/20240108081420_timerecording.php b/db/migrations/20240108081420_timerecording.php new file mode 100644 index 000000000..d24a2ab34 --- /dev/null +++ b/db/migrations/20240108081420_timerecording.php @@ -0,0 +1,42 @@ +getEnvironment() == "thetool") { + $timerecording = $this->table("Timerecording", ["signed" => true]); + $timerecording->addColumn("user_id", "integer", ["null" => false])->addIndex(["user_id"]); + $timerecording->addColumn("start", "integer", ["null" => true]); + $timerecording->addColumn("end", "integer", ["null" => true]); + $timerecording->addColumn("timerecordingCategory_id", "integer", ["null" => false]); + $timerecording->addColumn("commend", "text", ["null" => true]); + $timerecording->addColumn("approved", "integer", ["null" => false, "default" => "0"]); + $timerecording->addColumn("completed", "integer", ["null" => false, "default" => "0"]); + $timerecording->addColumn("create_by", "integer", ["null" => false]); + $timerecording->addColumn("edit_by", "integer", ["null" => false]); + $timerecording->addColumn("create", "integer", ["null" => false]); + $timerecording->addColumn("edit", "integer", ["null" => false]); + $timerecording->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $this->table("Timerecording")->drop()->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/db/migrations/20240108081441_timerecording_category.php b/db/migrations/20240108081441_timerecording_category.php new file mode 100644 index 000000000..bb3361cd5 --- /dev/null +++ b/db/migrations/20240108081441_timerecording_category.php @@ -0,0 +1,39 @@ +getEnvironment() == "thetool") { + $timerecordingCategory = $this->table("TimerecordingCategory", ["signed" => true]); + $timerecordingCategory->addColumn("name", "text", ["null" => false]); + $timerecordingCategory->addColumn("short", "string", ["null" => true, "limit" => 64]); + $timerecordingCategory->addColumn("hourday", "integer", ["null" => false, "default" => "1", "comment" => "1:Stunden/2:Tage/3:Startdatum/4:Enddatum"]); + $timerecordingCategory->addColumn("approval", "integer", ["null" => false, "default" => "0"]); + $timerecordingCategory->addColumn("require_comment", "integer", ["null" => false, "default" => "0"]); + $timerecordingCategory->addColumn("create_by", "integer", ["null" => false]); + $timerecordingCategory->addColumn("edit_by", "integer", ["null" => false]); + $timerecordingCategory->addColumn("create", "integer", ["null" => false]); + $timerecordingCategory->addColumn("edit", "integer", ["null" => false]); + $timerecordingCategory->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $this->table("TimerecordingCategory")->drop()->save(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } +}